Added a basic List element
This commit is contained in:
parent
90067a6ded
commit
9c24184f24
@ -39,7 +39,7 @@ func (element *Button) Resize (width, height int) {
|
|||||||
|
|
||||||
func (element *Button) HandleMouseDown (x, y int, button tomo.Button) {
|
func (element *Button) HandleMouseDown (x, y int, button tomo.Button) {
|
||||||
if !element.enabled { return }
|
if !element.enabled { return }
|
||||||
element.Select()
|
if !element.selected { element.Select() }
|
||||||
if button != tomo.ButtonLeft { return }
|
if button != tomo.ButtonLeft { return }
|
||||||
element.pressed = true
|
element.pressed = true
|
||||||
if element.core.HasImage() {
|
if element.core.HasImage() {
|
||||||
|
387
elements/basic/list.go
Normal file
387
elements/basic/list.go
Normal file
@ -0,0 +1,387 @@
|
|||||||
|
package basic
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
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"
|
||||||
|
|
||||||
|
// List is an element that contains several objects that a user can select.
|
||||||
|
type List struct {
|
||||||
|
*core.Core
|
||||||
|
core core.CoreControl
|
||||||
|
enabled bool
|
||||||
|
selected bool
|
||||||
|
contentHeight int
|
||||||
|
forcedMinimumWidth int
|
||||||
|
forcedMinimumHeight int
|
||||||
|
selectedEntry int
|
||||||
|
scroll int
|
||||||
|
entries []ListEntry
|
||||||
|
|
||||||
|
onSelectionRequest func () (granted bool)
|
||||||
|
onSelectedEntryChange func (index int)
|
||||||
|
onSelectionMotionRequest func (tomo.SelectionDirection) (granted bool)
|
||||||
|
onScrollBoundsChange func ()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewList creates a new list element with the specified entries.
|
||||||
|
func NewList (entries ...ListEntry) (element *List) {
|
||||||
|
element = &List { }
|
||||||
|
element.Core, element.core = core.NewCore(element)
|
||||||
|
|
||||||
|
element.entries = make([]ListEntry, len(entries))
|
||||||
|
for index, entry := range entries {
|
||||||
|
element.entries[index] = entry
|
||||||
|
}
|
||||||
|
|
||||||
|
element.updateMinimumSize()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resize changes the element's size.
|
||||||
|
func (element *List) Resize (width, height int) {
|
||||||
|
element.core.AllocateCanvas(width, height)
|
||||||
|
|
||||||
|
for index, entry := range element.entries {
|
||||||
|
element.entries[index] = element.resizeEntryToFit(entry)
|
||||||
|
}
|
||||||
|
|
||||||
|
element.draw()
|
||||||
|
if element.onScrollBoundsChange != nil {
|
||||||
|
element.onScrollBoundsChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: handle keyboard and mouse events
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
element.forcedMinimumWidth = width
|
||||||
|
element.forcedMinimumHeight = height
|
||||||
|
element.updateMinimumSize()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) HandleMouseDown (x, y int, button tomo.Button) {
|
||||||
|
if !element.enabled { return }
|
||||||
|
if !element.selected { element.Select() }
|
||||||
|
if button != tomo.ButtonLeft { return }
|
||||||
|
|
||||||
|
// if element.core.HasImage() {
|
||||||
|
// element.draw()
|
||||||
|
// element.core.DamageAll()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) HandleMouseUp (x, y int, button tomo.Button) {
|
||||||
|
if button != tomo.ButtonLeft { return }
|
||||||
|
// element.pressed = false
|
||||||
|
// if element.core.HasImage() {
|
||||||
|
// element.draw()
|
||||||
|
// element.core.DamageAll()
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// within := image.Point { x, y }.
|
||||||
|
// In(element.Bounds())
|
||||||
|
//
|
||||||
|
// if !element.enabled { return }
|
||||||
|
// if within && element.onClick != nil {
|
||||||
|
// element.onClick()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) HandleMouseMove (x, y int) { }
|
||||||
|
func (element *List) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
|
||||||
|
|
||||||
|
func (element *List) HandleKeyDown (key tomo.Key, modifiers tomo.Modifiers) {
|
||||||
|
if !element.enabled { return }
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) { }
|
||||||
|
|
||||||
|
func (element *List) Selected () (selected bool) {
|
||||||
|
return element.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) Select () {
|
||||||
|
if !element.enabled { return }
|
||||||
|
if element.onSelectionRequest != nil {
|
||||||
|
element.onSelectionRequest()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) HandleSelection (
|
||||||
|
direction tomo.SelectionDirection,
|
||||||
|
) (
|
||||||
|
accepted bool,
|
||||||
|
) {
|
||||||
|
direction = direction.Canon()
|
||||||
|
if !element.enabled { return false }
|
||||||
|
if element.selected && direction != tomo.SelectionDirectionNeutral {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
element.selected = true
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) HandleDeselection () {
|
||||||
|
element.selected = false
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) OnSelectionRequest (callback func () (granted bool)) {
|
||||||
|
element.onSelectionRequest = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) OnSelectionMotionRequest (
|
||||||
|
callback func (direction tomo.SelectionDirection) (granted bool),
|
||||||
|
) {
|
||||||
|
element.onSelectionMotionRequest = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnSelectedEntryChange sets the function to be called when the user selects an
|
||||||
|
// entry in this list.
|
||||||
|
func (element *List) OnSelectedEntryChange (callback func (index int)) {
|
||||||
|
element.onSelectedEntryChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
return element.Bounds().Dy() - theme.Padding()
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled sets whether this list can be interacted with or not.
|
||||||
|
func (element *List) SetEnabled (enabled bool) {
|
||||||
|
if element.enabled == enabled { return }
|
||||||
|
element.enabled = enabled
|
||||||
|
if element.core.HasImage () {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
entry.Collapse(element.forcedMinimumWidth)
|
||||||
|
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:]...)
|
||||||
|
entry.Collapse(element.forcedMinimumWidth)
|
||||||
|
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
|
||||||
|
entry.Collapse(element.forcedMinimumWidth)
|
||||||
|
element.entries[index] = entry
|
||||||
|
|
||||||
|
// redraw
|
||||||
|
element.updateMinimumSize()
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.draw()
|
||||||
|
element.core.DamageAll()
|
||||||
|
}
|
||||||
|
if element.onScrollBoundsChange != nil {
|
||||||
|
element.onScrollBoundsChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
|
||||||
|
entry.Collapse(element.forcedMinimumWidth)
|
||||||
|
return entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) updateMinimumSize () {
|
||||||
|
element.contentHeight = 0
|
||||||
|
for _, entry := range element.entries {
|
||||||
|
element.contentHeight += theme.Padding()
|
||||||
|
element.contentHeight += entry.Bounds().Dy()
|
||||||
|
}
|
||||||
|
|
||||||
|
minimumWidth := element.forcedMinimumWidth
|
||||||
|
minimumHeight := element.forcedMinimumHeight
|
||||||
|
|
||||||
|
if minimumWidth == 0 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if minimumHeight == 0 {
|
||||||
|
minimumHeight = element.contentHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
element.core.SetMinimumSize(minimumWidth, minimumHeight)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *List) draw () {
|
||||||
|
bounds := element.Bounds()
|
||||||
|
|
||||||
|
artist.FillRectangle (
|
||||||
|
element,
|
||||||
|
theme.ListPattern(),
|
||||||
|
bounds)
|
||||||
|
|
||||||
|
dot := image.Point {
|
||||||
|
bounds.Min.X + theme.Padding(),
|
||||||
|
bounds.Min.Y - element.scroll,
|
||||||
|
}
|
||||||
|
for index, entry := range element.entries {
|
||||||
|
dot.Y += theme.Padding()
|
||||||
|
entryPosition := dot
|
||||||
|
dot.Y += entry.Bounds().Dy()
|
||||||
|
if dot.Y < bounds.Min.Y { continue }
|
||||||
|
if entryPosition.Y > bounds.Max.Y { break }
|
||||||
|
|
||||||
|
selectionMarkerBounds := image.Rect (
|
||||||
|
theme.Padding() / 2,
|
||||||
|
entryPosition.Y - theme.Padding() / 2,
|
||||||
|
bounds.Dx() - theme.Padding() / 2,
|
||||||
|
entryPosition.Y + entry.Bounds().Dy() +
|
||||||
|
theme.Padding() / 2)
|
||||||
|
artist.FillRectangle (
|
||||||
|
element,
|
||||||
|
theme.ListEntryPattern(element.selectedEntry == index),
|
||||||
|
selectionMarkerBounds)
|
||||||
|
|
||||||
|
entry.Draw (
|
||||||
|
element, entryPosition,
|
||||||
|
element.selectedEntry == index && element.selected)
|
||||||
|
}
|
||||||
|
}
|
60
elements/basic/listentry.go
Normal file
60
elements/basic/listentry.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package basic
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
|
||||||
|
// ListEntry is an item that can be added to a list.
|
||||||
|
type ListEntry struct {
|
||||||
|
drawer artist.TextDrawer
|
||||||
|
bounds image.Rectangle
|
||||||
|
textPoint image.Point
|
||||||
|
text string
|
||||||
|
forcedMinimumWidth int
|
||||||
|
onClick func ()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewListEntry (text string, onClick func ()) (entry ListEntry) {
|
||||||
|
entry = ListEntry {
|
||||||
|
text: text,
|
||||||
|
onClick: onClick,
|
||||||
|
}
|
||||||
|
entry.drawer.SetText([]rune(text))
|
||||||
|
entry.drawer.SetFace(theme.FontFaceRegular())
|
||||||
|
entry.updateBounds()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *ListEntry) Collapse (width int) {
|
||||||
|
if entry.forcedMinimumWidth == width { return }
|
||||||
|
entry.forcedMinimumWidth = width
|
||||||
|
entry.updateBounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *ListEntry) updateBounds () {
|
||||||
|
entry.bounds = image.Rectangle { }
|
||||||
|
entry.bounds.Max.Y = entry.drawer.LineHeight().Round()
|
||||||
|
if entry.forcedMinimumWidth > 0 {
|
||||||
|
entry.bounds.Max.X = entry.drawer.LayoutBounds().Dx()
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.textPoint = image.Pt(0, 0).Sub(entry.drawer.LayoutBounds().Min)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *ListEntry) Draw (
|
||||||
|
destination tomo.Canvas,
|
||||||
|
offset image.Point,
|
||||||
|
selected bool,
|
||||||
|
) (
|
||||||
|
updatedRegion image.Rectangle,
|
||||||
|
) {
|
||||||
|
return entry.drawer.Draw (
|
||||||
|
destination,
|
||||||
|
theme.ForegroundPattern(true),
|
||||||
|
offset.Add(entry.textPoint))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entry *ListEntry) Bounds () (bounds image.Rectangle) {
|
||||||
|
return entry.bounds
|
||||||
|
}
|
@ -56,7 +56,7 @@ func (element *TextBox) Resize (width, height int) {
|
|||||||
|
|
||||||
func (element *TextBox) HandleMouseDown (x, y int, button tomo.Button) {
|
func (element *TextBox) HandleMouseDown (x, y int, button tomo.Button) {
|
||||||
if !element.enabled { return }
|
if !element.enabled { return }
|
||||||
element.Select()
|
if !element.selected { element.Select() }
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) HandleMouseUp (x, y int, button tomo.Button) { }
|
func (element *TextBox) HandleMouseUp (x, y int, button tomo.Button) { }
|
||||||
|
31
theme/list.go
Normal file
31
theme/list.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package theme
|
||||||
|
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
|
||||||
|
var listPattern = artist.NewMultiBordered (
|
||||||
|
artist.Stroke { Weight: 1, Pattern: strokePattern },
|
||||||
|
artist.Stroke {
|
||||||
|
Weight: 1,
|
||||||
|
Pattern: artist.Beveled {
|
||||||
|
Highlight: artist.NewUniform(hex(0x383C3AFF)),
|
||||||
|
Shadow: artist.NewUniform(hex(0x999C99FF)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
artist.Stroke { Pattern: artist.NewUniform(hex(0x999C99FF)) })
|
||||||
|
|
||||||
|
|
||||||
|
var listEntryPattern = artist.NewUniform(hex(0x999C99FF))
|
||||||
|
|
||||||
|
var selectedListEntryPattern = accentPattern
|
||||||
|
|
||||||
|
func ListPattern () (pattern artist.Pattern) {
|
||||||
|
return listPattern
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListEntryPattern (selected bool) (pattern artist.Pattern) {
|
||||||
|
if selected {
|
||||||
|
return selectedListEntryPattern
|
||||||
|
} else {
|
||||||
|
return listEntryPattern
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue
Block a user