Element methods are now more consistent and have less bool flags

Still need to update most examples...
This commit is contained in:
Sasha Koshka 2023-04-18 13:14:10 -04:00
parent a2b1ac0c73
commit 14080b1f88
17 changed files with 209 additions and 653 deletions

View File

@ -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()

View File

@ -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

View File

@ -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()

View File

@ -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

View File

@ -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,
}

View File

@ -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)

View File

@ -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)
}

View File

@ -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
}

View File

@ -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()

View File

@ -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 {

View File

@ -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")

View File

@ -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")

View File

@ -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

View File

@ -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()

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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()
}