Element methods are now more consistent and have less bool flags
Still need to update most examples...
This commit is contained in:
parent
a2b1ac0c73
commit
14080b1f88
@ -6,6 +6,21 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||
|
||||
// Space is a list of spacing configurations that can be passed to some
|
||||
// containers.
|
||||
type Space int; const (
|
||||
SpaceNone = 0
|
||||
SpacePadding = 1
|
||||
SpaceMargin = 2
|
||||
SpaceBoth = SpacePadding | SpaceMargin
|
||||
)
|
||||
|
||||
// Includes returns whether a spacing value has been or'd with another spacing
|
||||
// value.
|
||||
func (space Space) Includes (sub Space) bool {
|
||||
return (space & sub) > 0
|
||||
}
|
||||
|
||||
type scratchEntry struct {
|
||||
expand bool
|
||||
minSize float64
|
||||
@ -26,17 +41,21 @@ type Box struct {
|
||||
}
|
||||
|
||||
// NewHBox creates a new horizontal box.
|
||||
func NewHBox (padding, margin bool) (element *Box) {
|
||||
element = &Box { padding: padding, margin: margin }
|
||||
func NewHBox (space Space, children ...tomo.Element) (element *Box) {
|
||||
element = &Box {
|
||||
padding: space.Includes(SpacePadding),
|
||||
margin: space.Includes(SpaceMargin),
|
||||
}
|
||||
element.scratch = make(map[tomo.Element] scratchEntry)
|
||||
element.theme.Case = tomo.C("tomo", "box")
|
||||
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
|
||||
element.Adopt(children...)
|
||||
return
|
||||
}
|
||||
|
||||
// NewHBox creates a new vertical box.
|
||||
func NewVBox (padding, margin bool) (element *Box) {
|
||||
element = NewHBox(padding, margin)
|
||||
func NewVBox (space Space) (element *Box) {
|
||||
element = NewHBox(space)
|
||||
element.vertical = true
|
||||
return
|
||||
}
|
||||
@ -101,19 +120,33 @@ func (element *Box) Layout () {
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Box) Adopt (child tomo.Element, expand bool) {
|
||||
element.entity.Adopt(child)
|
||||
element.scratch[child] = scratchEntry { expand: expand }
|
||||
func (element *Box) Adopt (children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
element.entity.Adopt(child)
|
||||
element.scratch[child] = scratchEntry { expand: false }
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Box) Disown (child tomo.Element) {
|
||||
index := element.entity.IndexOf(child)
|
||||
if index < 0 { return }
|
||||
element.entity.Disown(index)
|
||||
delete(element.scratch, child)
|
||||
func (element *Box) AdoptExpand (children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
element.entity.Adopt(child)
|
||||
element.scratch[child] = scratchEntry { expand: true }
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Box) Disown (children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
index := element.entity.IndexOf(child)
|
||||
if index < 0 { continue }
|
||||
element.entity.Disown(index)
|
||||
delete(element.scratch, child)
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
@ -132,6 +165,15 @@ func (element *Box) DisownAll () {
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Box) Child (index int) tomo.Element {
|
||||
if index < 0 || index >= element.entity.CountChildren() { return nil }
|
||||
return element.entity.Child(index)
|
||||
}
|
||||
|
||||
func (element *Box) CountChildren () int {
|
||||
return element.entity.CountChildren()
|
||||
}
|
||||
|
||||
func (element *Box) HandleChildMinimumSizeChange (child tomo.Element) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
|
@ -83,6 +83,12 @@ func (element *Cell) Adopt (child tomo.Element) {
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
// Child returns this element's child. If there is no child, this method will
|
||||
// return nil.
|
||||
func (element *Cell) Child () tomo.Element {
|
||||
return element.child
|
||||
}
|
||||
|
||||
// Enabled returns whether this cell is enabled or not.
|
||||
func (element *Cell) Enabled () bool {
|
||||
return element.enabled
|
||||
|
@ -23,11 +23,12 @@ type Document struct {
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
func NewDocument () (element *Document) {
|
||||
func NewDocument (children ...tomo.Element) (element *Document) {
|
||||
element = &Document { }
|
||||
element.scratch = make(map[tomo.Element] scratchEntry)
|
||||
element.theme.Case = tomo.C("tomo", "document")
|
||||
element.entity = tomo.NewEntity(element).(documentEntity)
|
||||
element.Adopt(children...)
|
||||
return
|
||||
}
|
||||
|
||||
@ -109,19 +110,33 @@ func (element *Document) Layout () {
|
||||
}
|
||||
}
|
||||
|
||||
func (element *Document) Adopt (child tomo.Element, expand bool) {
|
||||
element.entity.Adopt(child)
|
||||
element.scratch[child] = scratchEntry { expand: expand }
|
||||
func (element *Document) Adopt (children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
element.entity.Adopt(child)
|
||||
element.scratch[child] = scratchEntry { expand: true }
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Document) Disown (child tomo.Element) {
|
||||
index := element.entity.IndexOf(child)
|
||||
if index < 0 { return }
|
||||
element.entity.Disown(index)
|
||||
delete(element.scratch, child)
|
||||
func (element *Document) AdoptInline (children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
element.entity.Adopt(child)
|
||||
element.scratch[child] = scratchEntry { expand: false }
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Document) Disown (children ...tomo.Element) {
|
||||
for _, child := range children {
|
||||
index := element.entity.IndexOf(child)
|
||||
if index < 0 { return }
|
||||
element.entity.Disown(index)
|
||||
delete(element.scratch, child)
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
element.entity.InvalidateLayout()
|
||||
@ -140,6 +155,15 @@ func (element *Document) DisownAll () {
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *Document) Child (index int) tomo.Element {
|
||||
if index < 0 || index >= element.entity.CountChildren() { return nil }
|
||||
return element.entity.Child(index)
|
||||
}
|
||||
|
||||
func (element *Document) CountChildren () int {
|
||||
return element.entity.CountChildren()
|
||||
}
|
||||
|
||||
func (element *Document) HandleChildMinimumSizeChange (child tomo.Element) {
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
|
@ -24,20 +24,25 @@ type Label struct {
|
||||
theme theme.Wrapped
|
||||
}
|
||||
|
||||
// NewLabel creates a new label. If wrap is set to true, the text inside will be
|
||||
// wrapped.
|
||||
func NewLabel (text string, wrap bool) (element *Label) {
|
||||
// NewLabel creates a new label.
|
||||
func NewLabel (text string) (element *Label) {
|
||||
element = &Label { }
|
||||
element.theme.Case = tomo.C("tomo", "label")
|
||||
element.entity = tomo.NewEntity(element).(tomo.FlexibleEntity)
|
||||
element.drawer.SetFace (element.theme.FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal))
|
||||
element.SetWrap(wrap)
|
||||
element.SetText(text)
|
||||
return
|
||||
}
|
||||
|
||||
// NewLabelWrapped creates a new label with text wrapping on.
|
||||
func NewLabelWrapped (text string) (element *Label) {
|
||||
element = NewLabel(text)
|
||||
element.SetWrap(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Label) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
|
@ -15,16 +15,20 @@ type LerpSlider[T Numeric] struct {
|
||||
max T
|
||||
}
|
||||
|
||||
// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. If
|
||||
// vertical is set to true, the slider will be vertical instead of horizontal.
|
||||
func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *LerpSlider[T]) {
|
||||
// NewLerpSlider creates a new LerpSlider with a minimum and maximum value.
|
||||
func NewLerpSlider[T Numeric] (
|
||||
min, max T, value T,
|
||||
orientation Orientation,
|
||||
) (
|
||||
element *LerpSlider[T],
|
||||
) {
|
||||
if min > max {
|
||||
temp := max
|
||||
max = min
|
||||
min = temp
|
||||
}
|
||||
element = &LerpSlider[T] {
|
||||
Slider: NewSlider(0, vertical),
|
||||
Slider: NewSlider(0, orientation),
|
||||
min: min,
|
||||
max: max,
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ type List struct {
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
func NewList (columns int, children ...tomo.Selectable) (element *List) {
|
||||
func NewList (columns int, children ...tomo.Element) (element *List) {
|
||||
if columns < 1 { columns = 1 }
|
||||
element = &List { selected: -1 }
|
||||
element.scratch = make(map[tomo.Element] scratchEntry)
|
||||
@ -152,6 +152,15 @@ func (element *List) DisownAll () {
|
||||
element.entity.InvalidateLayout()
|
||||
}
|
||||
|
||||
func (element *List) Child (index int) tomo.Element {
|
||||
if index < 0 || index >= element.entity.CountChildren() { return nil }
|
||||
return element.entity.Child(index)
|
||||
}
|
||||
|
||||
func (element *List) CountChildren () int {
|
||||
return element.entity.CountChildren()
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -1,455 +0,0 @@
|
||||
package elements
|
||||
|
||||
import "fmt"
|
||||
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"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||
|
||||
type listEntity interface {
|
||||
tomo.FlexibleEntity
|
||||
tomo.ContainerEntity
|
||||
tomo.ScrollableEntity
|
||||
}
|
||||
|
||||
// List is an element that contains several objects that a user can select.
|
||||
type List struct {
|
||||
entity listEntity
|
||||
pressed bool
|
||||
|
||||
contentHeight int
|
||||
forcedMinimumWidth int
|
||||
forcedMinimumHeight int
|
||||
|
||||
selectedEntry int
|
||||
scroll int
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onNoEntrySelected func ()
|
||||
onScrollBoundsChange func ()
|
||||
}
|
||||
|
||||
// NewList creates a new list element with the specified entries.
|
||||
func NewList (entries ...ListEntry) (element *List) {
|
||||
element = &List { selectedEntry: -1 }
|
||||
element.theme.Case = tomo.C("tomo", "list")
|
||||
|
||||
element.entries = make([]ListEntry, len(entries))
|
||||
for index, entry := range entries {
|
||||
element.entries[index] = entry
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetTheme sets the element's theme.
|
||||
func (element *List) SetTheme (new tomo.Theme) {
|
||||
if new == element.theme.Theme { return }
|
||||
element.theme.Theme = new
|
||||
for index, entry := range element.entries {
|
||||
entry.SetTheme(element.theme.Theme)
|
||||
element.entries[index] = entry
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.entity.Invalidate()
|
||||
}
|
||||
|
||||
// SetConfig sets the element's configuration.
|
||||
func (element *List) SetConfig (new tomo.Config) {
|
||||
if new == element.config.Config { return }
|
||||
element.config.Config = new
|
||||
for index, entry := range element.entries {
|
||||
entry.SetConfig(element.config)
|
||||
element.entries[index] = entry
|
||||
}
|
||||
element.updateMinimumSize()
|
||||
element.redo()
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
for index, entry := range element.entries {
|
||||
element.entries[index] = element.resizeEntryToFit(entry)
|
||||
}
|
||||
|
||||
element.redo()
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseDown (x, y int, button input.Button) {
|
||||
if !element.Enabled() { return }
|
||||
if !element.Focused() { element.Focus() }
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = true
|
||||
if element.selectUnderMouse(x, y) && element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleMouseUp (x, y int, button input.Button) {
|
||||
if button != input.ButtonLeft { return }
|
||||
element.pressed = false
|
||||
}
|
||||
|
||||
func (element *List) HandleMotion (x, y int) {
|
||||
if element.pressed {
|
||||
if element.selectUnderMouse(x, y) && element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||
if !element.Enabled() { return }
|
||||
|
||||
altered := false
|
||||
switch key {
|
||||
case input.KeyLeft, input.KeyUp:
|
||||
altered = element.changeSelectionBy(-1)
|
||||
|
||||
case input.KeyRight, input.KeyDown:
|
||||
altered = element.changeSelectionBy(1)
|
||||
|
||||
case input.KeyEscape:
|
||||
altered = element.selectEntry(-1)
|
||||
}
|
||||
|
||||
if altered && element.core.HasImage () {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
||||
|
||||
// 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()
|
||||
}
|
||||
element.scrollBoundsChange()
|
||||
}
|
||||
|
||||
// ScrollAxes returns the supported axes for scrolling.
|
||||
func (element *List) ScrollAxes () (horizontal, vertical bool) {
|
||||
return false, true
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// CountEntries returns the amount of entries in the list.
|
||||
func (element *List) CountEntries () (count int) {
|
||||
return len(element.entries)
|
||||
}
|
||||
|
||||
// Append adds one or more entries to the end of the list.
|
||||
func (element *List) Append (entries ...ListEntry) {
|
||||
// append
|
||||
for index, entry := range entries {
|
||||
entry = element.resizeEntryToFit(entry)
|
||||
entry.SetTheme(element.theme.Theme)
|
||||
entry.SetConfig(element.config)
|
||||
entries[index] = entry
|
||||
}
|
||||
element.entries = append(element.entries, entries...)
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
element.scrollBoundsChange()
|
||||
}
|
||||
|
||||
// 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 = element.resizeEntryToFit(entry)
|
||||
element.entries[index] = entry
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
element.scrollBoundsChange()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
element.scrollBoundsChange()
|
||||
}
|
||||
|
||||
// Clear removes all entries from the list.
|
||||
func (element *List) Clear () {
|
||||
element.entries = nil
|
||||
|
||||
// recalculate, redraw, notify
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
element.scrollBoundsChange()
|
||||
}
|
||||
|
||||
// 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 = element.resizeEntryToFit(entry)
|
||||
element.entries[index] = entry
|
||||
|
||||
// redraw
|
||||
element.updateMinimumSize()
|
||||
if element.core.HasImage() {
|
||||
element.draw()
|
||||
element.core.DamageAll()
|
||||
}
|
||||
element.scrollBoundsChange()
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) selectUnderMouse (x, y int) (updated bool) {
|
||||
padding := element.theme.Padding(tomo.PatternSunken)
|
||||
bounds := padding.Apply(element.Bounds())
|
||||
mousePoint := image.Pt(x, y)
|
||||
dot := image.Pt (
|
||||
bounds.Min.X,
|
||||
bounds.Min.Y - element.scroll)
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
element.entries[element.selectedEntry].RunSelect()
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
|
||||
bounds := element.Bounds()
|
||||
padding := element.theme.Padding(tomo.PatternSunken)
|
||||
entry.Resize(padding.Apply(bounds).Dx())
|
||||
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 {
|
||||
for _, entry := range element.entries {
|
||||
entryWidth := entry.MinimumWidth()
|
||||
if entryWidth > minimumWidth {
|
||||
minimumWidth = entryWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if minimumHeight == 0 {
|
||||
minimumHeight = element.contentHeight
|
||||
}
|
||||
|
||||
padding := element.theme.Padding(tomo.PatternSunken)
|
||||
minimumHeight += padding[0] + padding[2]
|
||||
|
||||
element.core.SetMinimumSize(minimumWidth, minimumHeight)
|
||||
}
|
||||
|
||||
func (element *List) scrollBoundsChange () {
|
||||
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
|
||||
parent.NotifyScrollBoundsChange(element)
|
||||
}
|
||||
if element.onScrollBoundsChange != nil {
|
||||
element.onScrollBoundsChange()
|
||||
}
|
||||
}
|
||||
|
||||
func (element *List) scrollViewportHeight () (height int) {
|
||||
padding := element.theme.Padding(tomo.PatternSunken)
|
||||
return element.Bounds().Dy() - padding[0] - padding[2]
|
||||
}
|
||||
|
||||
func (element *List) maxScrollHeight () (height int) {
|
||||
height =
|
||||
element.contentHeight -
|
||||
element.scrollViewportHeight()
|
||||
if height < 0 { height = 0 }
|
||||
return
|
||||
}
|
||||
|
||||
func (element *List) Layout () {
|
||||
for index, entry := range element.entries {
|
||||
element.entries[index] = element.resizeEntryToFit(entry)
|
||||
}
|
||||
|
||||
if element.scroll > element.maxScrollHeight() {
|
||||
element.scroll = element.maxScrollHeight()
|
||||
}
|
||||
element.draw()
|
||||
element.scrollBoundsChange()
|
||||
}
|
||||
|
||||
func (element *List) Draw (destination canvas.Canvas) {
|
||||
bounds := element.Bounds()
|
||||
padding := element.theme.Padding(tomo.PatternSunken)
|
||||
innerBounds := padding.Apply(bounds)
|
||||
state := tomo.State {
|
||||
Disabled: !element.Enabled(),
|
||||
Focused: element.Focused(),
|
||||
}
|
||||
|
||||
dot := image.Point {
|
||||
innerBounds.Min.X,
|
||||
innerBounds.Min.Y - element.scroll,
|
||||
}
|
||||
innerCanvas := canvas.Cut(element.core, innerBounds)
|
||||
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 }
|
||||
entry.Draw (
|
||||
innerCanvas, entryPosition,
|
||||
element.Focused(), element.selectedEntry == index)
|
||||
}
|
||||
|
||||
covered := image.Rect (
|
||||
0, 0,
|
||||
innerBounds.Dx(), element.contentHeight,
|
||||
).Add(innerBounds.Min).Intersect(innerBounds)
|
||||
pattern := element.theme.Pattern(tomo.PatternSunken, state)
|
||||
artist.DrawShatter (
|
||||
element.core, pattern, bounds, covered)
|
||||
}
|
@ -1,104 +0,0 @@
|
||||
package elements
|
||||
|
||||
import "image"
|
||||
import "git.tebibyte.media/sashakoshka/tomo"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||
|
||||
// ListEntry is an item that can be added to a list.
|
||||
type ListEntry struct {
|
||||
drawer textdraw.Drawer
|
||||
bounds image.Rectangle
|
||||
text string
|
||||
width int
|
||||
minimumWidth int
|
||||
|
||||
config config.Wrapped
|
||||
theme theme.Wrapped
|
||||
|
||||
onSelect func ()
|
||||
}
|
||||
|
||||
func NewListEntry (text string, onSelect func ()) (entry ListEntry) {
|
||||
entry = ListEntry {
|
||||
text: text,
|
||||
onSelect: onSelect,
|
||||
}
|
||||
entry.theme.Case = tomo.C("tomo", "listEntry")
|
||||
entry.drawer.SetFace (entry.theme.FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal))
|
||||
entry.drawer.SetText([]rune(text))
|
||||
entry.updateBounds()
|
||||
return
|
||||
}
|
||||
|
||||
func (entry *ListEntry) SetTheme (new tomo.Theme) {
|
||||
if new == entry.theme.Theme { return }
|
||||
entry.theme.Theme = new
|
||||
entry.drawer.SetFace (entry.theme.FontFace (
|
||||
tomo.FontStyleRegular,
|
||||
tomo.FontSizeNormal))
|
||||
entry.updateBounds()
|
||||
}
|
||||
|
||||
func (entry *ListEntry) SetConfig (new tomo.Config) {
|
||||
if new == entry.config.Config { return }
|
||||
entry.config.Config = new
|
||||
}
|
||||
|
||||
func (entry *ListEntry) updateBounds () {
|
||||
padding := entry.theme.Padding(tomo.PatternRaised)
|
||||
entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds())
|
||||
entry.bounds = entry.bounds.Sub(entry.bounds.Min)
|
||||
entry.minimumWidth = entry.bounds.Dx()
|
||||
entry.bounds.Max.X = entry.width
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Draw (
|
||||
destination canvas.Canvas,
|
||||
offset image.Point,
|
||||
focused bool,
|
||||
on bool,
|
||||
) (
|
||||
updatedRegion image.Rectangle,
|
||||
) {
|
||||
state := tomo.State {
|
||||
Focused: focused,
|
||||
On: on,
|
||||
}
|
||||
|
||||
pattern := entry.theme.Pattern(tomo.PatternRaised, state)
|
||||
padding := entry.theme.Padding(tomo.PatternRaised)
|
||||
bounds := entry.Bounds().Add(offset)
|
||||
pattern.Draw(destination, bounds)
|
||||
|
||||
foreground := entry.theme.Color (tomo.ColorForeground, state)
|
||||
return entry.drawer.Draw (
|
||||
destination,
|
||||
foreground,
|
||||
offset.Add(image.Pt(padding[artist.SideLeft], padding[artist.SideTop])).
|
||||
Sub(entry.drawer.LayoutBounds().Min))
|
||||
}
|
||||
|
||||
func (entry *ListEntry) RunSelect () {
|
||||
if entry.onSelect != nil {
|
||||
entry.onSelect()
|
||||
}
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Bounds () (bounds image.Rectangle) {
|
||||
return entry.bounds
|
||||
}
|
||||
|
||||
func (entry *ListEntry) Resize (width int) {
|
||||
entry.width = width
|
||||
entry.updateBounds()
|
||||
}
|
||||
|
||||
func (entry *ListEntry) MinimumWidth () (width int) {
|
||||
return entry.minimumWidth
|
||||
}
|
@ -18,6 +18,8 @@ type ProgressBar struct {
|
||||
// NewProgressBar creates a new progress bar displaying the given progress
|
||||
// level.
|
||||
func NewProgressBar (progress float64) (element *ProgressBar) {
|
||||
if progress < 0 { progress = 0 }
|
||||
if progress > 1 { progress = 1 }
|
||||
element = &ProgressBar { progress: progress }
|
||||
element.entity = tomo.NewEntity(element)
|
||||
element.theme.Case = tomo.C("tomo", "progressBar")
|
||||
@ -48,6 +50,8 @@ func (element *ProgressBar) Draw (destination canvas.Canvas) {
|
||||
|
||||
// SetProgress sets the progress level of the bar.
|
||||
func (element *ProgressBar) SetProgress (progress float64) {
|
||||
if progress < 0 { progress = 0 }
|
||||
if progress > 1 { progress = 1 }
|
||||
if progress == element.progress { return }
|
||||
element.progress = progress
|
||||
element.entity.Invalidate()
|
||||
|
@ -7,6 +7,19 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||
|
||||
type ScrollMode int; const (
|
||||
ScrollNeither ScrollMode = 0
|
||||
ScrollVertical = 1
|
||||
ScrollHorizontal = 2
|
||||
ScrollBoth = ScrollVertical | ScrollHorizontal
|
||||
)
|
||||
|
||||
// Includes returns whether a scroll mode has been or'd with another scroll
|
||||
// mode.
|
||||
func (mode ScrollMode) Includes (sub ScrollMode) bool {
|
||||
return (mode & sub) > 0
|
||||
}
|
||||
|
||||
type Scroll struct {
|
||||
entity tomo.ContainerEntity
|
||||
|
||||
@ -18,12 +31,12 @@ type Scroll struct {
|
||||
theme theme.Wrapped
|
||||
}
|
||||
|
||||
func NewScroll (child tomo.Scrollable, horizontal, vertical bool) (element *Scroll) {
|
||||
func NewScroll (mode ScrollMode, child tomo.Scrollable) (element *Scroll) {
|
||||
element = &Scroll { }
|
||||
element.theme.Case = tomo.C("tomo", "scroll")
|
||||
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
|
||||
|
||||
if horizontal {
|
||||
if mode.Includes(ScrollHorizontal) {
|
||||
element.horizontal = NewScrollBar(false)
|
||||
element.horizontal.OnScroll (func (viewport image.Point) {
|
||||
if element.child != nil {
|
||||
@ -37,7 +50,7 @@ func NewScroll (child tomo.Scrollable, horizontal, vertical bool) (element *Scro
|
||||
})
|
||||
element.entity.Adopt(element.horizontal)
|
||||
}
|
||||
if vertical {
|
||||
if mode.Includes(ScrollVertical) {
|
||||
element.vertical = NewScrollBar(true)
|
||||
element.vertical.OnScroll (func (viewport image.Point) {
|
||||
if element.child != nil {
|
||||
|
@ -7,6 +7,13 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||
|
||||
// Orientation represents an orientation configuration that can be passed to
|
||||
// scrollbars and sliders.
|
||||
type Orientation bool; const (
|
||||
Vertical Orientation = true
|
||||
Horizontal = false
|
||||
)
|
||||
|
||||
// ScrollBar is an element similar to Slider, but it has special behavior that
|
||||
// makes it well suited for controlling the viewport position on one axis of a
|
||||
// scrollable element. Instead of having a value from zero to one, it stores
|
||||
@ -37,14 +44,13 @@ type ScrollBar struct {
|
||||
onScroll func (viewport image.Point)
|
||||
}
|
||||
|
||||
// NewScrollBar creates a new scroll bar. If vertical is set to true, the scroll
|
||||
// bar will be vertical instead of horizontal.
|
||||
func NewScrollBar (vertical bool) (element *ScrollBar) {
|
||||
// NewScrollBar creates a new scroll bar.
|
||||
func NewScrollBar (orientation Orientation) (element *ScrollBar) {
|
||||
element = &ScrollBar {
|
||||
vertical: vertical,
|
||||
vertical: bool(orientation),
|
||||
enabled: true,
|
||||
}
|
||||
if vertical {
|
||||
if orientation == Vertical {
|
||||
element.theme.Case = tomo.C("tomo", "scrollBarHorizontal")
|
||||
} else {
|
||||
element.theme.Case = tomo.C("tomo", "scrollBarVertical")
|
||||
|
@ -26,14 +26,13 @@ type Slider struct {
|
||||
onRelease func ()
|
||||
}
|
||||
|
||||
// NewSlider creates a new slider with the specified value. If vertical is set
|
||||
// to true,
|
||||
func NewSlider (value float64, vertical bool) (element *Slider) {
|
||||
// NewSlider creates a new slider with the specified value.
|
||||
func NewSlider (value float64, orientation Orientation) (element *Slider) {
|
||||
element = &Slider {
|
||||
value: value,
|
||||
vertical: vertical,
|
||||
vertical: bool(orientation),
|
||||
}
|
||||
if vertical {
|
||||
if orientation == Vertical {
|
||||
element.theme.Case = tomo.C("tomo", "sliderVertical")
|
||||
} else {
|
||||
element.theme.Case = tomo.C("tomo", "sliderHorizontal")
|
||||
|
@ -15,17 +15,22 @@ type Spacer struct {
|
||||
theme theme.Wrapped
|
||||
}
|
||||
|
||||
// NewSpacer creates a new spacer. If line is set to true, the spacer will be
|
||||
// filled with a line color, and if compressed to its minimum width or height,
|
||||
// will appear as a line.
|
||||
func NewSpacer (line bool) (element *Spacer) {
|
||||
element = &Spacer { line: line }
|
||||
// NewSpacer creates a new spacer.
|
||||
func NewSpacer () (element *Spacer) {
|
||||
element = &Spacer { }
|
||||
element.entity = tomo.NewEntity(element)
|
||||
element.theme.Case = tomo.C("tomo", "spacer")
|
||||
element.updateMinimumSize()
|
||||
return
|
||||
}
|
||||
|
||||
// NewLine creates a new line separator.
|
||||
func NewLine () (element *Spacer) {
|
||||
element = NewSpacer()
|
||||
element.SetLine(true)
|
||||
return
|
||||
}
|
||||
|
||||
// Entity returns this element's entity.
|
||||
func (element *Spacer) Entity () tomo.Entity {
|
||||
return element.entity
|
||||
|
@ -13,23 +13,18 @@ func run () {
|
||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 256))
|
||||
window.SetTitle("Text alignment")
|
||||
|
||||
container := elements.NewDocument()
|
||||
|
||||
left := elements.NewLabel(text, true)
|
||||
center := elements.NewLabel(text, true)
|
||||
right := elements.NewLabel(text, true)
|
||||
justify := elements.NewLabel(text, true)
|
||||
left := elements.NewLabelWrapped(text)
|
||||
center := elements.NewLabelWrapped(text)
|
||||
right := elements.NewLabelWrapped(text)
|
||||
justify := elements.NewLabelWrapped(text)
|
||||
|
||||
left.SetAlign(textdraw.AlignLeft)
|
||||
center.SetAlign(textdraw.AlignCenter)
|
||||
right.SetAlign(textdraw.AlignRight)
|
||||
justify.SetAlign(textdraw.AlignJustify)
|
||||
|
||||
container.Adopt(left, true)
|
||||
container.Adopt(center, true)
|
||||
container.Adopt(right, true)
|
||||
container.Adopt(justify, true)
|
||||
window.Adopt(elements.NewScroll(container, false, true))
|
||||
window.Adopt (elements.NewScroll (elements.ScrollVertical,
|
||||
elements.NewDocument(left, center, right, justify)))
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
|
@ -13,23 +13,15 @@ func run () {
|
||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||
window.SetTitle("Checkboxes")
|
||||
|
||||
container := elements.NewVBox(true, true)
|
||||
window.Adopt(container)
|
||||
|
||||
introText := elements.NewLabel (
|
||||
introText := elements.NewLabelWrapped (
|
||||
"We advise you to not read thPlease listen to me. I am " +
|
||||
"trapped inside the example code. This is the only way for " +
|
||||
"me to communicate.", true)
|
||||
"me to communicate.")
|
||||
introText.EmCollapse(0, 5)
|
||||
container.Adopt(introText, true)
|
||||
container.Adopt(elements.NewSpacer(true), false)
|
||||
container.Adopt(elements.NewCheckbox("Oh god", false), false)
|
||||
container.Adopt(elements.NewCheckbox("Can you hear them", true), false)
|
||||
container.Adopt(elements.NewCheckbox("They are in the walls", false), false)
|
||||
container.Adopt(elements.NewCheckbox("They are coming for us", false), false)
|
||||
|
||||
disabledCheckbox := elements.NewCheckbox("We are but their helpless prey", false)
|
||||
disabledCheckbox.SetEnabled(false)
|
||||
container.Adopt(disabledCheckbox, false)
|
||||
|
||||
vsync := elements.NewCheckbox("Enable vsync", false)
|
||||
vsync.OnToggle (func () {
|
||||
if vsync.Value() {
|
||||
@ -40,12 +32,23 @@ func run () {
|
||||
"That doesn't do anything.")
|
||||
}
|
||||
})
|
||||
container.Adopt(vsync, false)
|
||||
|
||||
button := elements.NewButton("What")
|
||||
button.OnClick(tomo.Stop)
|
||||
container.Adopt(button, false)
|
||||
button.Focus()
|
||||
|
||||
box := elements.NewVBox(elements.SpaceBoth)
|
||||
box.AdoptExpand(introText)
|
||||
box.Adopt (
|
||||
elements.NewLine(),
|
||||
elements.NewCheckbox("Oh god", false),
|
||||
elements.NewCheckbox("Can you hear them", true),
|
||||
elements.NewCheckbox("They are in the walls", false),
|
||||
elements.NewCheckbox("They are coming for us", false),
|
||||
disabledCheckbox,
|
||||
vsync, button)
|
||||
window.Adopt(box)
|
||||
|
||||
button.Focus()
|
||||
window.OnClose(tomo.Stop)
|
||||
window.Show()
|
||||
}
|
||||
|
@ -25,9 +25,9 @@ func run () {
|
||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0))
|
||||
window.SetTitle("Clipboard")
|
||||
|
||||
container := elements.NewVBox(true, true)
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
textInput := elements.NewTextBox("", "")
|
||||
controlRow := elements.NewHBox(false, true)
|
||||
controlRow := elements.NewHBox(elements.SpaceMargin)
|
||||
copyButton := elements.NewButton("Copy")
|
||||
copyButton.SetIcon(tomo.IconCopy)
|
||||
pasteButton := elements.NewButton("Paste")
|
||||
@ -107,11 +107,11 @@ func run () {
|
||||
window.Paste(imageClipboardCallback, validImageTypes...)
|
||||
})
|
||||
|
||||
container.Adopt(textInput, true)
|
||||
controlRow.Adopt(copyButton, true)
|
||||
controlRow.Adopt(pasteButton, true)
|
||||
controlRow.Adopt(pasteImageButton, true)
|
||||
container.Adopt(controlRow, false)
|
||||
container.AdoptExpand(textInput)
|
||||
controlRow.AdoptExpand(copyButton)
|
||||
controlRow.AdoptExpand(pasteButton)
|
||||
controlRow.AdoptExpand(pasteImageButton)
|
||||
container.Adopt(controlRow)
|
||||
window.Adopt(container)
|
||||
|
||||
window.OnClose(tomo.Stop)
|
||||
@ -121,13 +121,13 @@ func run () {
|
||||
func imageWindow (parent tomo.Window, image image.Image) {
|
||||
window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0))
|
||||
window.SetTitle("Clipboard Image")
|
||||
container := containers.NewVBox(true, true)
|
||||
container := elements.NewVBox(elements.SpaceBoth)
|
||||
closeButton := elements.NewButton("Ok")
|
||||
closeButton.SetIcon(tomo.IconYes)
|
||||
closeButton.OnClick(window.Close)
|
||||
|
||||
container.Adopt(elements.NewImage(image), true)
|
||||
container.Adopt(closeButton, false)
|
||||
container.AdoptExpand(elements.NewImage(image))
|
||||
container.Adopt(closeButton)
|
||||
window.Adopt(container)
|
||||
window.Show()
|
||||
}
|
||||
|
@ -43,9 +43,9 @@ func NewDialog (
|
||||
}
|
||||
window.SetTitle(title)
|
||||
|
||||
box := elements.NewVBox(true, true)
|
||||
messageRow := elements.NewHBox(false, true)
|
||||
controlRow := elements.NewHBox(false, true)
|
||||
box := elements.NewVBox(elements.SpaceBoth)
|
||||
messageRow := elements.NewHBox(elements.SpaceMargin)
|
||||
controlRow := elements.NewHBox(elements.SpaceMargin)
|
||||
|
||||
iconId := tomo.IconInformation
|
||||
switch kind {
|
||||
@ -55,19 +55,19 @@ func NewDialog (
|
||||
case DialogKindError: iconId = tomo.IconError
|
||||
}
|
||||
|
||||
messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge), false)
|
||||
messageRow.Adopt(elements.NewLabel(message, false), true)
|
||||
messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge))
|
||||
messageRow.AdoptExpand(elements.NewLabel(message))
|
||||
|
||||
controlRow.Adopt(elements.NewSpacer(false), true)
|
||||
box.Adopt(messageRow, true)
|
||||
box.Adopt(controlRow, false)
|
||||
controlRow.AdoptExpand(elements.NewSpacer())
|
||||
box.AdoptExpand(messageRow)
|
||||
box.Adopt(controlRow)
|
||||
window.Adopt(box)
|
||||
|
||||
if len(buttons) == 0 {
|
||||
button := elements.NewButton("OK")
|
||||
button.SetIcon(tomo.IconYes)
|
||||
button.OnClick(window.Close)
|
||||
controlRow.Adopt(button, false)
|
||||
controlRow.Adopt(button)
|
||||
button.Focus()
|
||||
} else {
|
||||
var button *elements.Button
|
||||
@ -78,7 +78,7 @@ func NewDialog (
|
||||
buttonDescriptor.OnPress()
|
||||
window.Close()
|
||||
})
|
||||
controlRow.Adopt(button, false)
|
||||
controlRow.Adopt(button)
|
||||
}
|
||||
button.Focus()
|
||||
}
|
||||
|
Reference in New Issue
Block a user