Merge pull request 'ecs' (#15) from ecs into main

Reviewed-on: sashakoshka/tomo#15
This commit is contained in:
Sasha Koshka 2023-04-20 04:29:08 +00:00
commit 1323a6c1ca
81 changed files with 3849 additions and 5531 deletions

View File

@ -17,6 +17,9 @@ type Backend interface {
// possible. This method must be safe to call from other threads.
Do (callback func ())
// NewEntity creates a new entity for the specified element.
NewEntity (owner Element) Entity
// NewWindow creates a new window within the specified bounding
// rectangle. The position on screen may be overridden by the backend or
// operating system.

278
backends/x/entity.go Normal file
View File

@ -0,0 +1,278 @@
package x
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
type entity struct {
window *window
parent *entity
children []*entity
element tomo.Element
bounds image.Rectangle
clippedBounds image.Rectangle
minWidth int
minHeight int
selected bool
layoutInvalid bool
isContainer bool
}
func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity {
entity := &entity { element: owner }
if _, ok := owner.(tomo.Container); ok {
entity.isContainer = true
entity.InvalidateLayout()
}
return entity
}
func (ent *entity) unlink () {
ent.propagate (func (child *entity) bool {
if child.window != nil {
delete(ent.window.system.drawingInvalid, child)
}
child.window = nil
return true
})
if ent.window != nil {
delete(ent.window.system.drawingInvalid, ent)
}
ent.parent = nil
ent.window = nil
if element, ok := ent.element.(tomo.Selectable); ok {
ent.selected = false
element.HandleSelectionChange()
}
}
func (entity *entity) link (parent *entity) {
entity.parent = parent
entity.clip(parent.clippedBounds)
if parent.window != nil {
entity.setWindow(parent.window)
}
}
func (ent *entity) setWindow (window *window) {
ent.window = window
ent.Invalidate()
ent.InvalidateLayout()
ent.propagate (func (child *entity) bool {
child.window = window
ent.Invalidate()
ent.InvalidateLayout()
return true
})
}
func (entity *entity) propagate (callback func (*entity) bool) bool {
for _, child := range entity.children {
if !child.propagate(callback) {
return false
}
}
return callback(entity)
}
func (entity *entity) childAt (point image.Point) *entity {
for _, child := range entity.children {
if point.In(child.bounds) {
return child.childAt(point)
}
}
return entity
}
func (entity *entity) scrollTargetChildAt (point image.Point) *entity {
for _, child := range entity.children {
if point.In(child.bounds) {
result := child.scrollTargetChildAt(point)
if result != nil { return result }
break
}
}
if _, ok := entity.element.(tomo.ScrollTarget); ok {
return entity
}
return nil
}
func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer, tomo.Element)) {
if entity.parent == nil { return }
if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok {
callback(parent, entity.element)
}
entity.parent.forMouseTargetContainers(callback)
}
func (entity *entity) clip (bounds image.Rectangle) {
entity.clippedBounds = entity.bounds.Intersect(bounds)
for _, child := range entity.children {
child.clip(entity.clippedBounds)
}
}
// ----------- Entity ----------- //
func (entity *entity) Invalidate () {
if entity.window == nil { return }
if entity.window.system.invalidateIgnore { return }
entity.window.drawingInvalid.Add(entity)
}
func (entity *entity) Bounds () image.Rectangle {
return entity.bounds
}
func (entity *entity) Window () tomo.Window {
return entity.window
}
func (entity *entity) SetMinimumSize (width, height int) {
entity.minWidth = width
entity.minHeight = height
if entity.parent == nil {
if entity.window != nil {
entity.window.setMinimumSize(width, height)
}
} else {
entity.parent.element.(tomo.Container).
HandleChildMinimumSizeChange(entity.element)
}
}
func (entity *entity) DrawBackground (destination canvas.Canvas) {
if entity.parent != nil {
entity.parent.element.(tomo.Container).DrawBackground(destination)
} else if entity.window != nil {
entity.window.system.theme.Pattern (
tomo.PatternBackground,
tomo.State { }).Draw (
destination,
entity.window.canvas.Bounds())
}
}
// ----------- ContainerEntity ----------- //
func (entity *entity) InvalidateLayout () {
if entity.window == nil { return }
if !entity.isContainer { return }
entity.layoutInvalid = true
entity.window.system.anyLayoutInvalid = true
}
func (ent *entity) Adopt (child tomo.Element) {
childEntity, ok := child.Entity().(*entity)
if !ok || childEntity == nil { return }
childEntity.link(ent)
ent.children = append(ent.children, childEntity)
}
func (ent *entity) Insert (index int, child tomo.Element) {
childEntity, ok := child.Entity().(*entity)
if !ok || childEntity == nil { return }
ent.children = append (
ent.children[:index + 1],
ent.children[index:]...)
ent.children[index] = childEntity
}
func (entity *entity) Disown (index int) {
entity.children[index].unlink()
entity.children = append (
entity.children[:index],
entity.children[index + 1:]...)
}
func (entity *entity) IndexOf (child tomo.Element) int {
for index, childEntity := range entity.children {
if childEntity.element == child {
return index
}
}
return -1
}
func (entity *entity) Child (index int) tomo.Element {
return entity.children[index].element
}
func (entity *entity) CountChildren () int {
return len(entity.children)
}
func (entity *entity) PlaceChild (index int, bounds image.Rectangle) {
child := entity.children[index]
child.bounds = bounds
child.clip(entity.clippedBounds)
child.Invalidate()
child.InvalidateLayout()
}
func (entity *entity) SelectChild (index int, selected bool) {
child := entity.children[index]
if element, ok := child.element.(tomo.Selectable); ok {
if child.selected == selected { return }
child.selected = selected
element.HandleSelectionChange()
}
}
func (entity *entity) ChildMinimumSize (index int) (width, height int) {
childEntity := entity.children[index]
return childEntity.minWidth, childEntity.minHeight
}
// ----------- FocusableEntity ----------- //
func (entity *entity) Focused () bool {
if entity.window == nil { return false }
return entity.window.focused == entity
}
func (entity *entity) Focus () {
if entity.window == nil { return }
entity.window.system.focus(entity)
}
func (entity *entity) FocusNext () {
entity.window.system.focusNext()
}
func (entity *entity) FocusPrevious () {
entity.window.system.focusPrevious()
}
// ----------- SelectableEntity ----------- //
func (entity *entity) Selected () bool {
return entity.selected
}
// ----------- FlexibleEntity ----------- //
func (entity *entity) NotifyFlexibleHeightChange () {
if entity.parent == nil { return }
if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok {
parent.HandleChildFlexibleHeightChange (
entity.element.(tomo.Flexible))
}
}
// ----------- ScrollableEntity ----------- //
func (entity *entity) NotifyScrollBoundsChange () {
if entity.parent == nil { return }
if parent, ok := entity.parent.element.(tomo.ScrollableContainer); ok {
parent.HandleChildScrollBoundsChange (
entity.element.(tomo.Scrollable))
}
}

View File

@ -20,28 +20,19 @@ func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
(state & window.backend.modifierMasks.shiftLock) > 0
if shift {
switch button {
case 4:
sum.x -= scrollDistance
case 5:
sum.x += scrollDistance
case 6:
sum.y -= scrollDistance
case 7:
sum.y += scrollDistance
case 4: sum.x -= scrollDistance
case 5: sum.x += scrollDistance
case 6: sum.y -= scrollDistance
case 7: sum.y += scrollDistance
}
} else {
switch button {
case 4:
sum.y -= scrollDistance
case 5:
sum.y += scrollDistance
case 6:
sum.x -= scrollDistance
case 7:
sum.x += scrollDistance
case 4: sum.y -= scrollDistance
case 5: sum.y += scrollDistance
case 6: sum.x -= scrollDistance
case 7: sum.x += scrollDistance
}
}
}
func (window *window) handleExpose (
@ -49,6 +40,7 @@ func (window *window) handleExpose (
event xevent.ExposeEvent,
) {
_, region := window.compressExpose(*event.ExposeEvent)
window.system.afterEvent()
window.pushRegion(region)
}
@ -74,7 +66,6 @@ func (window *window) handleConfigureNotify (
window.updateBounds (
configureEvent.X, configureEvent.Y,
configureEvent.Width, configureEvent.Height)
if sizeChanged {
configureEvent = window.compressConfigureNotify(configureEvent)
@ -85,8 +76,11 @@ func (window *window) handleConfigureNotify (
window.resizeChildToFit()
if !window.exposeEventFollows(configureEvent) {
window.redrawChildEntirely()
window.child.Invalidate()
window.child.InvalidateLayout()
}
window.system.afterEvent()
}
}
@ -127,8 +121,7 @@ func (window *window) handleKeyPress (
connection *xgbutil.XUtil,
event xevent.KeyPressEvent,
) {
if window.child == nil { return }
if window.hasModal { return }
if window.hasModal { return }
keyEvent := *event.KeyPressEvent
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
@ -136,29 +129,25 @@ func (window *window) handleKeyPress (
modifiers.NumberPad = numberPad
if key == input.KeyTab && modifiers.Alt {
if child, ok := window.child.(tomo.Focusable); ok {
direction := input.KeynavDirectionForward
if modifiers.Shift {
direction = input.KeynavDirectionBackward
}
if !child.HandleFocus(direction) {
child.HandleUnfocus()
}
if modifiers.Shift {
window.system.focusPrevious()
} else {
window.system.focusNext()
}
} else if key == input.KeyEscape && window.shy {
window.Close()
} else if child, ok := window.child.(tomo.KeyboardTarget); ok {
child.HandleKeyDown(key, modifiers)
} else if window.focused != nil {
focused, ok := window.focused.element.(tomo.KeyboardTarget)
if ok { focused.HandleKeyDown(key, modifiers) }
}
window.system.afterEvent()
}
func (window *window) handleKeyRelease (
connection *xgbutil.XUtil,
event xevent.KeyReleaseEvent,
) {
if window.child == nil { return }
keyEvent := *event.KeyReleaseEvent
// do not process this event if it was generated from a key repeat
@ -181,9 +170,12 @@ func (window *window) handleKeyRelease (
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
modifiers := window.modifiersFromState(keyEvent.State)
modifiers.NumberPad = numberPad
if child, ok := window.child.(tomo.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers)
if window.focused != nil {
focused, ok := window.focused.element.(tomo.KeyboardTarget)
if ok { focused.HandleKeyUp(key, modifiers) }
window.system.afterEvent()
}
}
@ -191,68 +183,99 @@ func (window *window) handleButtonPress (
connection *xgbutil.XUtil,
event xevent.ButtonPressEvent,
) {
if window.child == nil { return }
if window.hasModal { return }
if window.hasModal { return }
buttonEvent := *event.ButtonPressEvent
insideWindow := image.Pt (
int(buttonEvent.EventX),
int(buttonEvent.EventY)).In(window.canvas.Bounds())
buttonEvent := *event.ButtonPressEvent
point := image.Pt(int(buttonEvent.EventX), int(buttonEvent.EventY))
insideWindow := point.In(window.canvas.Bounds())
scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7
scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7
if !insideWindow && window.shy && !scrolling {
window.Close()
} else if scrolling {
if child, ok := window.child.(tomo.ScrollTarget); ok {
sum := scrollSum { }
sum.add(buttonEvent.Detail, window, buttonEvent.State)
window.compressScrollSum(buttonEvent, &sum)
child.HandleScroll (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
float64(sum.x), float64(sum.y))
underneath := window.system.scrollTargetChildAt(point)
if underneath != nil {
if child, ok := underneath.element.(tomo.ScrollTarget); ok {
sum := scrollSum { }
sum.add(buttonEvent.Detail, window, buttonEvent.State)
window.compressScrollSum(buttonEvent, &sum)
child.HandleScroll (
point.X, point.Y,
float64(sum.x), float64(sum.y))
}
}
} else {
if child, ok := window.child.(tomo.MouseTarget); ok {
underneath := window.system.childAt(point)
window.system.drags[buttonEvent.Detail] = underneath
if child, ok := underneath.element.(tomo.MouseTarget); ok {
child.HandleMouseDown (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
point.X, point.Y,
input.Button(buttonEvent.Detail))
}
callback := func (container tomo.MouseTargetContainer, child tomo.Element) {
container.HandleChildMouseDown (
point.X, point.Y,
input.Button(buttonEvent.Detail),
child)
}
underneath.forMouseTargetContainers(callback)
}
window.system.afterEvent()
}
func (window *window) handleButtonRelease (
connection *xgbutil.XUtil,
event xevent.ButtonReleaseEvent,
) {
if window.child == nil { return }
if child, ok := window.child.(tomo.MouseTarget); ok {
buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
child.HandleMouseUp (
buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
dragging := window.system.drags[buttonEvent.Detail]
if dragging != nil {
if child, ok := dragging.element.(tomo.MouseTarget); ok {
child.HandleMouseUp (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
input.Button(buttonEvent.Detail))
}
callback := func (container tomo.MouseTargetContainer, child tomo.Element) {
container.HandleChildMouseUp (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
input.Button(buttonEvent.Detail))
input.Button(buttonEvent.Detail),
child)
}
dragging.forMouseTargetContainers(callback)
}
window.system.afterEvent()
}
func (window *window) handleMotionNotify (
connection *xgbutil.XUtil,
event xevent.MotionNotifyEvent,
) {
if window.child == nil { return }
if child, ok := window.child.(tomo.MotionTarget); ok {
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
child.HandleMotion (
int(motionEvent.EventX),
int(motionEvent.EventY))
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
x := int(motionEvent.EventX)
y :=int(motionEvent.EventY)
handled := false
for _, child := range window.system.drags {
if child == nil { continue }
if child, ok := child.element.(tomo.MotionTarget); ok {
child.HandleMotion(x, y)
handled = true
}
}
if !handled {
child := window.system.childAt(image.Pt(x, y))
if child, ok := child.element.(tomo.MotionTarget); ok {
child.HandleMotion(x, y)
}
}
window.system.afterEvent()
}
func (window *window) handleSelectionNotify (

View File

@ -97,11 +97,13 @@ func (request *selectionRequest) convertSelection (
func (request *selectionRequest) die (err error) {
request.callback(nil, err)
request.window.system.afterEvent()
request.state = selReqStateClosed
}
func (request *selectionRequest) finalize (data data.Data) {
request.callback(data, nil)
request.window.system.afterEvent()
request.state = selReqStateClosed
}

186
backends/x/system.go Normal file
View File

@ -0,0 +1,186 @@
package x
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type entitySet map[*entity] struct { }
func (set entitySet) Empty () bool {
return len(set) == 0
}
func (set entitySet) Has (entity *entity) bool {
_, ok := set[entity]
return ok
}
func (set entitySet) Add (entity *entity) {
set[entity] = struct { } { }
}
type system struct {
child *entity
focused *entity
canvas canvas.BasicCanvas
theme theme.Wrapped
config config.Wrapped
invalidateIgnore bool
drawingInvalid entitySet
anyLayoutInvalid bool
drags [10]*entity
pushFunc func (image.Rectangle)
}
func (system *system) initialize () {
system.drawingInvalid = make(entitySet)
}
func (system *system) SetTheme (theme tomo.Theme) {
system.theme.Theme = theme
system.propagate (func (entity *entity) bool {
if child, ok := system.child.element.(tomo.Themeable); ok {
child.SetTheme(theme)
}
return true
})
}
func (system *system) SetConfig (config tomo.Config) {
system.config.Config = config
system.propagate (func (entity *entity) bool {
if child, ok := system.child.element.(tomo.Configurable); ok {
child.SetConfig(config)
}
return true
})
}
func (system *system) focus (entity *entity) {
previous := system.focused
system.focused = entity
if previous != nil {
previous.element.(tomo.Focusable).HandleFocusChange()
}
if entity != nil {
entity.element.(tomo.Focusable).HandleFocusChange()
}
}
func (system *system) focusNext () {
found := system.focused == nil
focused := false
system.propagate (func (entity *entity) bool {
if found {
// looking for the next element to select
child, ok := entity.element.(tomo.Focusable)
if ok && child.Enabled() {
// found it
entity.Focus()
focused = true
return false
}
} else {
// looking for the current focused element
if entity == system.focused {
// found it
found = true
}
}
return true
})
if !focused { system.focus(nil) }
}
func (system *system) focusPrevious () {
var behind *entity
system.propagate (func (entity *entity) bool {
if entity == system.focused {
return false
}
child, ok := entity.element.(tomo.Focusable)
if ok && child.Enabled() { behind = entity }
return true
})
system.focus(behind)
}
func (system *system) propagate (callback func (*entity) bool) {
if system.child == nil { return }
system.child.propagate(callback)
}
func (system *system) childAt (point image.Point) *entity {
if system.child == nil { return nil }
return system.child.childAt(point)
}
func (system *system) scrollTargetChildAt (point image.Point) *entity {
if system.child == nil { return nil }
return system.child.scrollTargetChildAt(point)
}
func (system *system) resizeChildToFit () {
system.child.bounds = system.canvas.Bounds()
system.child.clippedBounds = system.child.bounds
system.child.Invalidate()
if system.child.isContainer {
system.child.InvalidateLayout()
}
}
func (system *system) afterEvent () {
if system.anyLayoutInvalid {
system.layout(system.child, false)
system.anyLayoutInvalid = false
}
system.draw()
}
func (system *system) layout (entity *entity, force bool) {
if entity == nil { return }
if entity.layoutInvalid == true || force {
if element, ok := entity.element.(tomo.Layoutable); ok {
element.Layout()
entity.layoutInvalid = false
force = true
}
}
for _, child := range entity.children {
system.layout(child, force)
}
}
func (system *system) draw () {
finalBounds := image.Rectangle { }
// ignore invalidations that result from drawing elements, because if an
// element decides to do that it really needs to rethink its life
// choices.
system.invalidateIgnore = true
defer func () { system.invalidateIgnore = false } ()
for entity := range system.drawingInvalid {
if entity.clippedBounds.Empty() { continue }
entity.element.Draw (canvas.Cut (
system.canvas,
entity.clippedBounds))
finalBounds = finalBounds.Union(entity.clippedBounds)
}
system.drawingInvalid = make(entitySet)
// TODO: don't just union all the bounds together, we can definetly
// consolidateupdated regions more efficiently than this.
if !finalBounds.Empty() {
system.pushFunc(finalBounds)
}
}

View File

@ -13,20 +13,16 @@ import "github.com/jezek/xgbutil/mousebind"
import "github.com/jezek/xgbutil/xgraphics"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// import "runtime/debug"
type mainWindow struct { *window }
type menuWindow struct { *window }
type window struct {
system
backend *Backend
xWindow *xwindow.Window
xCanvas *xgraphics.Image
canvas canvas.BasicCanvas
child tomo.Element
onClose func ()
skipChildDrawCallback bool
title, application string
@ -34,15 +30,14 @@ type window struct {
hasModal bool
shy bool
theme tomo.Theme
config tomo.Config
selectionRequest *selectionRequest
selectionClaim *selectionClaim
metrics struct {
bounds image.Rectangle
}
onClose func ()
}
func (backend *Backend) NewWindow (
@ -53,6 +48,7 @@ func (backend *Backend) NewWindow (
) {
if backend == nil { panic("nil backend") }
window, err := backend.newWindow(bounds, false)
output = mainWindow { window }
return output, err
}
@ -69,6 +65,10 @@ func (backend *Backend) newWindow (
window := &window { backend: backend }
window.system.initialize()
window.system.pushFunc = window.pasteAndPush
window.theme.Case = tomo.C("tomo", "window")
window.xWindow, err = xwindow.Generate(backend.connection)
if err != nil { return }
@ -126,7 +126,7 @@ func (backend *Backend) newWindow (
window.SetConfig(backend.config)
window.metrics.bounds = bounds
window.childMinimumSizeChangeCallback(8, 8)
window.setMinimumSize(8, 8)
window.reallocateCanvas()
@ -136,69 +136,31 @@ func (backend *Backend) newWindow (
return
}
func (window *window) NotifyMinimumSizeChange (child tomo.Element) {
window.childMinimumSizeChangeCallback(child.MinimumSize())
}
func (window *window) Window () tomo.Window {
return window
}
func (window *window) RequestFocus (
child tomo.Focusable,
) (
granted bool,
) {
return true
}
func (window *window) RequestFocusNext (child tomo.Focusable) {
if child, ok := window.child.(tomo.Focusable); ok {
if !child.HandleFocus(input.KeynavDirectionForward) {
child.HandleUnfocus()
}
}
}
func (window *window) RequestFocusPrevious (child tomo.Focusable) {
if child, ok := window.child.(tomo.Focusable); ok {
if !child.HandleFocus(input.KeynavDirectionBackward) {
child.HandleUnfocus()
}
}
}
func (window *window) Adopt (child tomo.Element) {
// disown previous child
if window.child != nil {
window.child.SetParent(nil)
window.child.DrawTo(nil, image.Rectangle { }, nil)
window.child.unlink()
window.child = nil
}
// adopt new child
if child != nil {
// adopt new child
window.child = child
child.SetParent(window)
if newChild, ok := child.(tomo.Themeable); ok {
newChild.SetTheme(window.theme)
}
if newChild, ok := child.(tomo.Configurable); ok {
newChild.SetConfig(window.config)
}
if child != nil {
if !window.childMinimumSizeChangeCallback(child.MinimumSize()) {
window.resizeChildToFit()
window.redrawChildEntirely()
}
childEntity, ok := child.Entity().(*entity)
if ok && childEntity != nil {
window.child = childEntity
childEntity.setWindow(window)
window.setMinimumSize (
childEntity.minWidth,
childEntity.minHeight)
window.resizeChildToFit()
}
}
}
func (window *window) Child () (child tomo.Element) {
child = window.child
return
}
func (window *window) SetTitle (title string) {
window.title = title
ewmh.WmNameSet (
@ -317,43 +279,6 @@ func (window menuWindow) Pin () {
// TODO iungrab keyboard and mouse
}
func (window *window) grabInput () {
keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id)
mousebind.GrabPointer (
window.backend.connection,
window.xWindow.Id,
window.backend.connection.RootWin(), 0)
}
func (window *window) ungrabInput () {
keybind.UngrabKeyboard(window.backend.connection)
mousebind.UngrabPointer(window.backend.connection)
}
func (window *window) inheritProperties (parent *window) {
window.SetApplicationName(parent.application)
}
func (window *window) setType (ty string) error {
return ewmh.WmWindowTypeSet (
window.backend.connection,
window.xWindow.Id,
[]string { "_NET_WM_WINDOW_TYPE_" + ty })
}
func (window *window) setClientLeader (leader *window) error {
hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id)
if hints == nil {
hints = &icccm.Hints { }
}
hints.Flags |= icccm.HintWindowGroup
hints.WindowGroup = leader.xWindow.Id
return icccm.WmHintsSet (
window.backend.connection,
window.xWindow.Id,
hints)
}
func (window *window) Show () {
if window.child == nil {
window.xCanvas.For (func (x, y int) xgraphics.BGRA {
@ -362,7 +287,7 @@ func (window *window) Show () {
window.pushRegion(window.xCanvas.Bounds())
}
window.xWindow.Map()
if window.shy { window.grabInput() }
}
@ -417,18 +342,41 @@ func (window *window) OnClose (callback func ()) {
window.onClose = callback
}
func (window *window) SetTheme (theme tomo.Theme) {
window.theme = theme
if child, ok := window.child.(tomo.Themeable); ok {
child.SetTheme(theme)
}
func (window *window) grabInput () {
keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id)
mousebind.GrabPointer (
window.backend.connection,
window.xWindow.Id,
window.backend.connection.RootWin(), 0)
}
func (window *window) SetConfig (config tomo.Config) {
window.config = config
if child, ok := window.child.(tomo.Configurable); ok {
child.SetConfig(config)
func (window *window) ungrabInput () {
keybind.UngrabKeyboard(window.backend.connection)
mousebind.UngrabPointer(window.backend.connection)
}
func (window *window) inheritProperties (parent *window) {
window.SetApplicationName(parent.application)
}
func (window *window) setType (ty string) error {
return ewmh.WmWindowTypeSet (
window.backend.connection,
window.xWindow.Id,
[]string { "_NET_WM_WINDOW_TYPE_" + ty })
}
func (window *window) setClientLeader (leader *window) error {
hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id)
if hints == nil {
hints = &icccm.Hints { }
}
hints.Flags |= icccm.HintWindowGroup
hints.WindowGroup = leader.xWindow.Id
return icccm.WmHintsSet (
window.backend.connection,
window.xWindow.Id,
hints)
}
func (window *window) reallocateCanvas () {
@ -464,22 +412,7 @@ func (window *window) reallocateCanvas () {
}
func (window *window) redrawChildEntirely () {
window.paste(window.canvas.Bounds())
window.pushRegion(window.canvas.Bounds())
}
func (window *window) resizeChildToFit () {
window.skipChildDrawCallback = true
window.child.DrawTo (
window.canvas,
window.canvas.Bounds(),
window.childDrawCallback)
window.skipChildDrawCallback = false
}
func (window *window) childDrawCallback (region image.Rectangle) {
if window.skipChildDrawCallback { return }
func (window *window) pasteAndPush (region image.Rectangle) {
window.paste(region)
window.pushRegion(region)
}
@ -492,7 +425,6 @@ func (window *window) paste (region image.Rectangle) {
dstStride := window.xCanvas.Stride
dstData := window.xCanvas.Pix
// debug.PrintStack()
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
srcYComponent := y * stride
dstYComponent := y * dstStride
@ -507,7 +439,21 @@ func (window *window) paste (region image.Rectangle) {
}
}
func (window *window) childMinimumSizeChangeCallback (width, height int) (resized bool) {
func (window *window) pushRegion (region image.Rectangle) {
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)
if ok {
image.XDraw()
image.XExpPaint (
window.xWindow.Id,
image.Bounds().Min.X,
image.Bounds().Min.Y)
}
}
func (window *window) setMinimumSize (width, height int) {
if width < 8 { width = 8 }
if height < 8 { height = 8 }
icccm.WmNormalHintsSet (
window.backend.connection,
window.xWindow.Id,
@ -523,20 +469,5 @@ func (window *window) childMinimumSizeChangeCallback (width, height int) (resize
if newWidth != window.metrics.bounds.Dx() ||
newHeight != window.metrics.bounds.Dy() {
window.xWindow.Resize(newWidth, newHeight)
return true
}
return false
}
func (window *window) pushRegion (region image.Rectangle) {
if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") }
image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image)
if ok {
image.XDraw()
image.XExpPaint (
window.xWindow.Id,
image.Bounds().Min.X,
image.Bounds().Min.Y)
}
}

View File

@ -67,6 +67,9 @@ func (backend *Backend) Run () (err error) {
<- pingAfter
case callback := <- backend.doChannel:
callback()
for _, window := range backend.windows {
window.system.afterEvent()
}
case <- pingQuit:
return
}

View File

@ -212,14 +212,9 @@ func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.P
switch id {
case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF)
case tomo.PatternDead: return defaultTextures[0][offset]
case tomo.PatternRaised:
if c.Match("tomo", "listEntry", "") {
return defaultTextures[10][offset]
} else {
return defaultTextures[1][offset]
}
case tomo.PatternSunken: return defaultTextures[2][offset]
case tomo.PatternPinboard: return defaultTextures[3][offset]
case tomo.PatternRaised: return defaultTextures[1][offset]
case tomo.PatternSunken: return defaultTextures[2][offset]
case tomo.PatternPinboard: return defaultTextures[3][offset]
case tomo.PatternButton:
switch {
case c.Match("tomo", "checkbox", ""):
@ -272,16 +267,8 @@ func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA {
// Padding returns the default padding value for the given pattern.
func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
switch id {
case tomo.PatternRaised:
if c.Match("tomo", "listEntry", "") {
return artist.I(4, 8)
} else {
return artist.I(8)
}
case tomo.PatternSunken:
if c.Match("tomo", "list", "") {
return artist.I(4, 0, 3)
} else if c.Match("tomo", "progressBar", "") {
if c.Match("tomo", "progressBar", "") {
return artist.I(2, 1, 1, 2)
} else {
return artist.I(8)
@ -292,16 +279,26 @@ func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
} else {
return artist.I(8)
}
case tomo.PatternTableCell: return artist.I(5)
case tomo.PatternTableHead: return artist.I(5)
case tomo.PatternGutter: return artist.I(0)
case tomo.PatternLine: return artist.I(1)
case tomo.PatternMercury: return artist.I(5)
default: return artist.I(8)
default: return artist.I(8)
}
}
// Margin returns the default margin value for the given pattern.
func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point {
return image.Pt(8, 8)
switch id {
case tomo.PatternSunken:
if c.Match("tomo", "list", "") {
return image.Pt(-1, -1)
} else {
return image.Pt(8, 8)
}
default: return image.Pt(8, 8)
}
}
// Hints returns rendering optimization hints for a particular pattern.

View File

@ -6,54 +6,75 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas"
// Element represents a basic on-screen object.
type Element interface {
// Bounds reports the element's bounding box. This must reflect the
// bounding last given to the element by DrawTo.
Bounds () image.Rectangle
// MinimumSize specifies the minimum amount of pixels this element's
// width and height may be set to. If the element is given a resize
// event with dimensions smaller than this, it will use its minimum
// instead of the offending dimension(s).
MinimumSize () (width, height int)
// Draw causes the element to draw to the specified canvas. The bounds
// of this canvas specify the area that is actually drawn to, while the
// Entity bounds specify the actual area of the element.
Draw (canvas.Canvas)
// SetParent sets the parent container of the element. This should only
// be called by the parent when the element is adopted. If parent is set
// to nil, it will mark itself as not having a parent. If this method is
// passed a non-nil value and the element already has a parent, it will
// panic.
SetParent (Parent)
// DrawTo gives the element a canvas to draw on, along with a bounding
// box to be used for laying out the element. This should only be called
// by the parent element. This is typically a region of the parent
// element's canvas.
DrawTo (canvas canvas.Canvas, bounds image.Rectangle, onDamage func (region image.Rectangle))
// Entity returns this element's entity.
Entity () Entity
}
// Focusable represents an element that has keyboard navigation support. This
// includes inputs, buttons, sliders, etc. as well as any elements that have
// children (so keyboard navigation events can be propagated downward).
type Focusable interface {
// Layoutable represents an element that needs to perform layout calculations
// before it can draw itself.
type Layoutable interface {
Element
// Layout causes this element to perform a layout operation.
Layout ()
}
// Container represents an element capable of containing child elements.
type Container interface {
Element
Layoutable
// DrawBackground causes the element to draw its background pattern to
// the specified canvas. The bounds of this canvas specify the area that
// is actually drawn to, while the Entity bounds specify the actual area
// of the element.
DrawBackground (canvas.Canvas)
// HandleChildMinimumSizeChange is called when a child's minimum size is
// changed.
HandleChildMinimumSizeChange (child Element)
}
// Enableable represents an element that can be enabled and disabled. Disabled
// elements typically appear greyed out.
type Enableable interface {
Element
// Focused returns whether or not this element or any of its children
// are currently focused.
Focused () bool
// Enabled returns whether or not the element is enabled.
Enabled () bool
// SetEnabled sets whether or not the element is enabled.
SetEnabled (bool)
}
// Focus focuses this element, if its parent element grants the
// request.
Focus ()
// Focusable represents an element that has keyboard navigation support.
type Focusable interface {
Element
Enableable
// HandleFocus causes this element to mark itself as focused. If the
// element does not have children, it is disabled, or there are no more
// selectable children in the given direction, it should return false
// and do nothing. Otherwise, it should select itself and any children
// (if applicable) and return true.
HandleFocus (direction input.KeynavDirection) (accepted bool)
// HandleFocusChange is called when the element is focused or unfocused.
HandleFocusChange ()
}
// HandleDeselection causes this element to mark itself and all of its
// children as unfocused.
HandleUnfocus ()
// Selectable represents an element that can be selected. This includes things
// like list items, files, etc. The difference between this and Focusable is
// that multiple Selectable elements may be selected at the same time, whereas
// only one Focusable element may be focused at the same time. Containers who's
// purpose is to contain selectable elements can determine when to select them
// by implementing MouseTargetContainer and listening for HandleChildMouseDown
// events.
type Selectable interface {
Element
Enableable
// HandleSelectionChange is called when the element is selected or
// deselected.
HandleSelectionChange ()
}
// KeyboardTarget represents an element that can receive keyboard input.
@ -85,6 +106,22 @@ type MouseTarget interface {
HandleMouseUp (x, y int, button input.Button)
}
// MouseTargetContainer represents an element that wants to know when one
// of its children is clicked. Children do not have to implement MouseTarget for
// a container satisfying MouseTargetContainer to be notified that they have
// been clicked.
type MouseTargetContainer interface {
Container
// HandleMouseDown is called when a mouse button is pressed down on a
// child element.
HandleChildMouseDown (x, y int, button input.Button, child Element)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on a child element.
HandleChildMouseUp (x, y int, button input.Button, child Element)
}
// MotionTarget represents an element that can receive mouse motion events.
type MotionTarget interface {
Element
@ -125,6 +162,16 @@ type Flexible interface {
FlexibleHeightFor (width int) int
}
// FlexibleContainer represents an element that is capable of containing
// flexible children.
type FlexibleContainer interface {
Container
// HandleChildFlexibleHeightChange is called when the parameters
// affecting a child's flexible height are changed.
HandleChildFlexibleHeightChange (child Flexible)
}
// Scrollable represents an element that can be scrolled. It acts as a viewport
// through which its contents can be observed.
type Scrollable interface {
@ -145,6 +192,16 @@ type Scrollable interface {
ScrollAxes () (horizontal, vertical bool)
}
// ScrollableContainer represents an element that is capable of containing
// scrollable children.
type ScrollableContainer interface {
Container
// HandleChildScrollBoundsChange is called when the content bounds,
// viewport bounds, or scroll axes of a child are changed.
HandleChildScrollBoundsChange (child Scrollable)
}
// Collapsible represents an element who's minimum width and height can be
// manually resized. Scrollable elements should implement this if possible.
type Collapsible interface {

216
elements/box.go Normal file
View File

@ -0,0 +1,216 @@
package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
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
}
// Box is a container that lays out its children horizontally or vertically.
// Child elements can be set to contract to their minimum size, or expand to
// fill remaining space. Boxes can be nested and used together to create more
// complex layouts.
type Box struct {
container
theme theme.Wrapped
padding bool
margin bool
vertical bool
}
// NewHBox creates a new horizontal box.
func NewHBox (space Space, children ...tomo.Element) (element *Box) {
element = &Box {
padding: space.Includes(SpacePadding),
margin: space.Includes(SpaceMargin),
}
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
element.minimumSize = element.updateMinimumSize
element.init()
element.theme.Case = tomo.C("tomo", "box")
element.Adopt(children...)
return
}
// NewHBox creates a new vertical box.
func NewVBox (space Space, children ...tomo.Element) (element *Box) {
element = &Box {
padding: space.Includes(SpacePadding),
margin: space.Includes(SpaceMargin),
vertical: true,
}
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
element.minimumSize = element.updateMinimumSize
element.init()
element.theme.Case = tomo.C("tomo", "box")
element.Adopt(children...)
return
}
func (element *Box) Draw (destination canvas.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.entity.DrawBackground(canvas.Cut(destination, tile))
}
}
func (element *Box) Layout () {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
bounds := element.entity.Bounds()
if element.padding { bounds = padding.Apply(bounds) }
var marginSize float64; if element.vertical {
marginSize = float64(margin.Y)
} else {
marginSize = float64(margin.X)
}
freeSpace, nExpanding := element.freeSpace()
expandingElementSize := freeSpace / nExpanding
// set the size and position of each element
x := float64(bounds.Min.X)
y := float64(bounds.Min.Y)
for index := 0; index < element.entity.CountChildren(); index ++ {
entry := element.scratch[element.entity.Child(index)]
var size float64; if entry.expand {
size = expandingElementSize
} else {
size = entry.minSize
}
var childBounds image.Rectangle; if element.vertical {
childBounds = tomo.Bounds(int(x), int(y), bounds.Dx(), int(size))
} else {
childBounds = tomo.Bounds(int(x), int(y), int(size), bounds.Dy())
}
element.entity.PlaceChild(index, childBounds)
if element.vertical {
y += size
if element.margin { y += marginSize }
} else {
x += size
if element.margin { x += marginSize }
}
}
}
func (element *Box) AdoptExpand (children ...tomo.Element) {
element.adopt(true, children...)
}
func (element *Box) DrawBackground (destination canvas.Canvas) {
element.entity.DrawBackground(destination)
}
// SetTheme sets the element's theme.
func (element *Box) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Box) freeSpace () (space float64, nExpanding float64) {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
var marginSize int; if element.vertical {
marginSize = margin.Y
} else {
marginSize = margin.X
}
if element.vertical {
space = float64(element.entity.Bounds().Dy())
} else {
space = float64(element.entity.Bounds().Dx())
}
for _, entry := range element.scratch {
if entry.expand {
nExpanding ++;
} else {
space -= float64(entry.minSize)
}
}
if element.padding {
space -= float64(padding.Vertical())
}
if element.margin {
space -= float64(marginSize * (len(element.scratch) - 1))
}
return
}
func (element *Box) updateMinimumSize () {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
var breadth, size int
var marginSize int; if element.vertical {
marginSize = margin.Y
} else {
marginSize = margin.X
}
for index := 0; index < element.entity.CountChildren(); index ++ {
childWidth, childHeight := element.entity.ChildMinimumSize(index)
var childBreadth, childSize int; if element.vertical {
childBreadth, childSize = childWidth, childHeight
} else {
childBreadth, childSize = childHeight, childWidth
}
key := element.entity.Child(index)
entry := element.scratch[key]
entry.minSize = float64(childSize)
element.scratch[key] = entry
if childBreadth > breadth {
breadth = childBreadth
}
size += childSize
if element.margin && index > 0 {
size += marginSize
}
}
var width, height int; if element.vertical {
width, height = breadth, size
} else {
width, height = size, breadth
}
if element.padding {
width += padding.Horizontal()
height += padding.Vertical()
}
element.entity.SetMinimumSize(width, height)
}

View File

@ -1,24 +1,19 @@
package elements
import "image"
// import "runtime/debug"
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/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// import "git.tebibyte.media/sashakoshka/tomo/artist"
// import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Button is a clickable button.
type Button struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
entity tomo.FocusableEntity
drawer textdraw.Drawer
enabled bool
pressed bool
text string
@ -34,11 +29,9 @@ type Button struct {
// NewButton creates a new button with the specified label text.
func NewButton (text string) (element *Button) {
element = &Button { showText: true }
element = &Button { showText: true, enabled: true }
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.theme.Case = tomo.C("tomo", "button")
element.Core, element.core = core.NewCore(element, element.drawAll)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush)
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
@ -46,163 +39,19 @@ func NewButton (text string) (element *Button) {
return
}
func (element *Button) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if button != input.ButtonLeft { return }
element.pressed = true
element.drawAndPush()
// Entity returns this element's entity.
func (element *Button) Entity () tomo.Entity {
return element.entity
}
func (element *Button) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft { return }
element.pressed = false
within := image.Point { x, y }.
In(element.Bounds())
if element.Enabled() && within && element.onClick != nil {
element.onClick()
}
element.drawAndPush()
}
func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
if key == input.KeyEnter {
element.pressed = true
element.drawAndPush()
}
}
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.drawAndPush()
if !element.Enabled() { return }
if element.onClick != nil {
element.onClick()
}
}
}
// OnClick sets the function to be called when the button is clicked.
func (element *Button) OnClick (callback func ()) {
element.onClick = callback
}
// SetEnabled sets whether this button can be clicked or not.
func (element *Button) SetEnabled (enabled bool) {
element.focusableControl.SetEnabled(enabled)
}
// SetText sets the button's label text.
func (element *Button) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.drawAndPush()
}
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
// current icon if it exists.
func (element *Button) SetIcon (id tomo.Icon) {
if id == tomo.IconNone {
element.hasIcon = false
element.updateMinimumSize()
element.drawAndPush()
} else {
if element.hasIcon && element.iconId == id { return }
element.hasIcon = true
element.iconId = id
}
element.updateMinimumSize()
element.drawAndPush()
}
// ShowText sets whether or not the button's text will be displayed.
func (element *Button) ShowText (showText bool) {
if element.showText == showText { return }
element.showText = showText
element.updateMinimumSize()
element.drawAndPush()
}
// SetTheme sets the element's theme.
func (element *Button) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.drawAndPush()
}
// SetConfig sets the element's configuration.
func (element *Button) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.drawAndPush()
}
func (element *Button) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
textBounds := element.drawer.LayoutBounds()
minimumSize := textBounds.Sub(textBounds.Min)
if element.hasIcon {
icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall)
if icon != nil {
bounds := icon.Bounds()
if element.showText {
minimumSize.Max.X += bounds.Dx()
minimumSize.Max.X += margin.X
} else {
minimumSize.Max.X = bounds.Dx()
}
}
}
minimumSize = padding.Inverse().Apply(minimumSize)
element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
}
func (element *Button) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
}
}
func (element *Button) drawAndPush () {
if element.core.HasImage () {
element.drawAll()
element.core.DamageAll()
}
}
func (element *Button) drawAll () {
element.drawBackground()
element.drawText()
}
func (element *Button) drawBackground () []image.Rectangle {
// Draw causes the element to draw to the specified destination canvas.
func (element *Button) Draw (destination canvas.Canvas) {
state := element.state()
bounds := element.Bounds()
bounds := element.entity.Bounds()
pattern := element.theme.Pattern(tomo.PatternButton, state)
pattern.Draw(element.core, bounds)
return []image.Rectangle { bounds }
}
func (element *Button) drawText () {
state := element.state()
bounds := element.Bounds()
pattern.Draw(destination, bounds)
foreground := element.theme.Color(tomo.ColorForeground, state)
sink := element.theme.Sink(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
@ -240,7 +89,7 @@ func (element *Button) drawText () {
}
offset.X += addedWidth / 2
icon.Draw(element.core, foreground, iconOffset)
icon.Draw(destination, foreground, iconOffset)
}
}
@ -248,6 +97,151 @@ func (element *Button) drawText () {
if element.pressed {
offset = offset.Add(sink)
}
element.drawer.Draw(element.core, foreground, offset)
element.drawer.Draw(destination, foreground, offset)
}
}
// OnClick sets the function to be called when the button is clicked.
func (element *Button) OnClick (callback func ()) {
element.onClick = callback
}
// Focus gives this element input focus.
func (element *Button) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this button is enabled or not.
func (element *Button) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this button can be clicked or not.
func (element *Button) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// SetText sets the button's label text.
func (element *Button) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
// current icon if it exists.
func (element *Button) SetIcon (id tomo.Icon) {
if id == tomo.IconNone {
element.hasIcon = false
} else {
if element.hasIcon && element.iconId == id { return }
element.hasIcon = true
element.iconId = id
}
element.updateMinimumSize()
element.entity.Invalidate()
}
// ShowText sets whether or not the button's text will be displayed.
func (element *Button) ShowText (showText bool) {
if element.showText == showText { return }
element.showText = showText
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Button) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Button) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Button) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *Button) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
if button != input.ButtonLeft { return }
element.pressed = true
element.entity.Invalidate()
}
func (element *Button) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft { return }
element.pressed = false
within := image.Point { x, y }.In(element.entity.Bounds())
if element.Enabled() && within && element.onClick != nil {
element.onClick()
}
element.entity.Invalidate()
}
func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
if key == input.KeyEnter {
element.pressed = true
element.entity.Invalidate()
}
}
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.entity.Invalidate()
if !element.Enabled() { return }
if element.onClick != nil {
element.onClick()
}
}
}
func (element *Button) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternButton)
margin := element.theme.Margin(tomo.PatternButton)
textBounds := element.drawer.LayoutBounds()
minimumSize := textBounds.Sub(textBounds.Min)
if element.hasIcon {
icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall)
if icon != nil {
bounds := icon.Bounds()
if element.showText {
minimumSize.Max.X += bounds.Dx()
minimumSize.Max.X += margin.X
} else {
minimumSize.Max.X = bounds.Dx()
}
}
}
minimumSize = padding.Inverse().Apply(minimumSize)
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
}
func (element *Button) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
}
}

165
elements/cell.go Normal file
View File

@ -0,0 +1,165 @@
package elements
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/default/theme"
type cellEntity interface {
tomo.ContainerEntity
tomo.SelectableEntity
}
// Cell is a single-element container that satisfies tomo.Selectable. It
// provides styling based on whether or not it is selected.
type Cell struct {
entity cellEntity
child tomo.Element
enabled bool
theme theme.Wrapped
onSelectionChange func ()
}
// NewCell creates a new cell element. If padding is true, the cell will have
// padding on all sides. Child can be nil and added later with the Adopt()
// method.
func NewCell (child tomo.Element) (element *Cell) {
element = &Cell { enabled: true }
element.theme.Case = tomo.C("tomo", "cell")
element.entity = tomo.NewEntity(element).(cellEntity)
element.Adopt(child)
return
}
// Entity returns this element's entity.
func (element *Cell) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Cell) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
pattern := element.theme.Pattern(tomo.PatternTableCell, element.state())
if element.child == nil {
pattern.Draw(destination, bounds)
} else {
artist.DrawShatter (
destination, pattern, bounds,
element.child.Entity().Bounds())
}
}
// Draw causes the element to perform a layout operation.
func (element *Cell) Layout () {
if element.child == nil { return }
bounds := element.entity.Bounds()
bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds)
element.entity.PlaceChild(0, bounds)
}
// DrawBackground draws this element's background pattern to the specified
// destination canvas.
func (element *Cell) DrawBackground (destination canvas.Canvas) {
element.theme.Pattern(tomo.PatternTableCell, element.state()).
Draw(destination, element.entity.Bounds())
}
// Adopt sets this element's child. If nil is passed, any child is removed.
func (element *Cell) Adopt (child tomo.Element) {
if element.child != nil {
element.entity.Disown(element.entity.IndexOf(element.child))
}
if child != nil {
element.entity.Adopt(child)
}
element.child = child
element.updateMinimumSize()
element.entity.Invalidate()
element.invalidateChild()
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
}
// SetEnabled sets whether this cell can be selected or not.
func (element *Cell) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
element.invalidateChild()
}
// SetTheme sets this element's theme.
func (element *Cell) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize()
element.entity.Invalidate()
element.invalidateChild()
element.entity.InvalidateLayout()
}
// OnSelectionChange sets a function to be called when this element is selected
// or unselected.
func (element *Cell) OnSelectionChange (callback func ()) {
element.onSelectionChange = callback
}
func (element *Cell) Selected () bool {
return element.entity.Selected()
}
func (element *Cell) HandleSelectionChange () {
element.entity.Invalidate()
element.invalidateChild()
if element.onSelectionChange != nil {
element.onSelectionChange()
}
}
func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Cell) state () tomo.State {
return tomo.State {
Disabled: !element.enabled,
On: element.entity.Selected(),
}
}
func (element *Cell) updateMinimumSize () {
width, height := 0, 0
if element.child != nil {
childWidth, childHeight := element.entity.ChildMinimumSize(0)
width += childWidth
height += childHeight
}
padding := element.theme.Padding(tomo.PatternTableCell)
width += padding.Horizontal()
height += padding.Vertical()
element.entity.SetMinimumSize(width, height)
}
func (element *Cell) invalidateChild () {
if element.child != nil {
element.child.Entity().Invalidate()
}
}

View File

@ -3,19 +3,17 @@ package elements
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/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// Checkbox is a toggle-able checkbox with a label.
type Checkbox struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
entity tomo.FocusableEntity
drawer textdraw.Drawer
enabled bool
pressed bool
checked bool
text string
@ -28,11 +26,9 @@ type Checkbox struct {
// NewCheckbox creates a new cbeckbox with the specified label text.
func NewCheckbox (text string, checked bool) (element *Checkbox) {
element = &Checkbox { checked: checked }
element = &Checkbox { checked: checked, enabled: true }
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.theme.Case = tomo.C("tomo", "checkbox")
element.Core, element.core = core.NewCore(element, element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.core, element.redo)
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
@ -40,57 +36,39 @@ func NewCheckbox (text string, checked bool) (element *Checkbox) {
return
}
func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
// Entity returns this element's entity.
func (element *Checkbox) Entity () tomo.Entity {
return element.entity
}
func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft || !element.pressed { return }
// Draw causes the element to draw to the specified destination canvas.
func (element *Checkbox) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
element.pressed = false
within := image.Point { x, y }.
In(element.Bounds())
if within {
element.checked = !element.checked
state := tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
On: element.checked,
}
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter {
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
}
element.entity.DrawBackground(destination)
pattern := element.theme.Pattern(tomo.PatternButton, state)
pattern.Draw(destination, boxBounds)
func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if element.onToggle != nil {
element.onToggle()
}
}
textBounds := element.drawer.LayoutBounds()
margin := element.theme.Margin(tomo.PatternBackground)
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() + margin.X,
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := element.theme.Color(tomo.ColorForeground, state)
element.drawer.Draw(destination, foreground, offset)
}
// OnToggle sets the function to be called when the checkbox is toggled.
@ -103,23 +81,30 @@ func (element *Checkbox) Value () (checked bool) {
return element.checked
}
// Focus gives this element input focus.
func (element *Checkbox) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this checkbox is enabled or not.
func (element *Checkbox) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this checkbox can be toggled or not.
func (element *Checkbox) SetEnabled (enabled bool) {
element.focusableControl.SetEnabled(enabled)
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// SetText sets the checkbox's label text.
func (element *Checkbox) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
@ -130,7 +115,7 @@ func (element *Checkbox) SetTheme (new tomo.Theme) {
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.redo()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
@ -138,54 +123,61 @@ func (element *Checkbox) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
element.entity.Invalidate()
}
func (element *Checkbox) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
element.entity.Invalidate()
}
func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false
within := image.Point { x, y }.In(element.entity.Bounds())
if within {
element.checked = !element.checked
}
element.entity.Invalidate()
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter {
element.pressed = true
element.entity.Invalidate()
}
}
func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
element.entity.Invalidate()
if element.onToggle != nil {
element.onToggle()
}
}
}
func (element *Checkbox) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
if element.text == "" {
element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
} else {
margin := element.theme.Margin(tomo.PatternBackground)
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
textBounds.Dy() + margin.X + textBounds.Dx(),
textBounds.Dy())
}
}
func (element *Checkbox) redo () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Checkbox) draw () {
bounds := element.Bounds()
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
state := tomo.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
Pressed: element.pressed,
On: element.checked,
}
element.core.DrawBackground (
element.theme.Pattern(tomo.PatternBackground, state))
pattern := element.theme.Pattern(tomo.PatternButton, state)
pattern.Draw(element.core, boxBounds)
textBounds := element.drawer.LayoutBounds()
margin := element.theme.Margin(tomo.PatternBackground)
offset := bounds.Min.Add(image.Point {
X: bounds.Dy() + margin.X,
})
offset.Y -= textBounds.Min.Y
offset.X -= textBounds.Min.X
foreground := element.theme.Color(tomo.ColorForeground, state)
element.drawer.Draw(element.core, foreground, offset)
}

77
elements/container.go Normal file
View File

@ -0,0 +1,77 @@
package elements
import "git.tebibyte.media/sashakoshka/tomo"
type scratchEntry struct {
expand bool
minSize float64
minBreadth float64
}
type container struct {
entity tomo.ContainerEntity
scratch map[tomo.Element] scratchEntry
minimumSize func ()
}
func (container *container) Entity () tomo.Entity {
return container.entity
}
func (container *container) Adopt (children ...tomo.Element) {
container.adopt(false, children...)
}
func (container *container) init () {
container.scratch = make(map[tomo.Element] scratchEntry)
}
func (container *container) adopt (expand bool, children ...tomo.Element) {
for _, child := range children {
container.entity.Adopt(child)
container.scratch[child] = scratchEntry { expand: expand }
}
container.minimumSize()
container.entity.Invalidate()
container.entity.InvalidateLayout()
}
func (container *container) Disown (children ...tomo.Element) {
for _, child := range children {
index := container.entity.IndexOf(child)
if index < 0 { continue }
container.entity.Disown(index)
delete(container.scratch, child)
}
container.minimumSize()
container.entity.Invalidate()
container.entity.InvalidateLayout()
}
func (container *container) DisownAll () {
func () {
for index := 0; index < container.entity.CountChildren(); index ++ {
index := index
defer container.entity.Disown(index)
}
} ()
container.scratch = make(map[tomo.Element] scratchEntry)
container.minimumSize()
container.entity.Invalidate()
container.entity.InvalidateLayout()
}
func (container *container) Child (index int) tomo.Element {
if index < 0 || index >= container.entity.CountChildren() { return nil }
return container.entity.Child(index)
}
func (container *container) CountChildren () int {
return container.entity.CountChildren()
}
func (container *container) HandleChildMinimumSizeChange (child tomo.Element) {
container.minimumSize()
container.entity.Invalidate()
container.entity.InvalidateLayout()
}

View File

@ -1,258 +0,0 @@
package containers
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// Container is an element capable of containg other elements, and arranging
// them in a layout.
type Container struct {
*core.Core
*core.Propagator
core core.CoreControl
layout tomo.Layout
children []tomo.LayoutEntry
warping bool
config config.Wrapped
theme theme.Wrapped
onFocusRequest func () (granted bool)
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
}
// NewContainer creates a new container.
func NewContainer (layout tomo.Layout) (element *Container) {
element = &Container { }
element.theme.Case = tomo.C("tomo", "container")
element.Core, element.core = core.NewCore(element, element.redoAll)
element.Propagator = core.NewPropagator(element, element.core)
element.SetLayout(layout)
return
}
// SetLayout sets the layout of this container.
func (element *Container) SetLayout (layout tomo.Layout) {
element.layout = layout
element.updateMinimumSize()
if element.core.HasImage() {
element.redoAll()
element.core.DamageAll()
}
}
// Adopt adds a new child element to the container. If expand is set to true,
// the element will expand (instead of contract to its minimum size), in
// whatever way is defined by the current layout.
func (element *Container) Adopt (child tomo.Element, expand bool) {
if child0, ok := child.(tomo.Themeable); ok {
child0.SetTheme(element.theme.Theme)
}
if child0, ok := child.(tomo.Configurable); ok {
child0.SetConfig(element.config.Config)
}
child.SetParent(element)
// add child
element.children = append (element.children, tomo.LayoutEntry {
Element: child,
Expand: expand,
})
// refresh stale data
element.updateMinimumSize()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
// Warp runs the specified callback, deferring all layout and rendering updates
// until the callback has finished executing. This allows for aplications to
// perform batch gui updates without flickering and stuff.
func (element *Container) Warp (callback func ()) {
if element.warping {
callback()
return
}
element.warping = true
callback()
element.warping = false
// TODO: create some sort of task list so we don't do a full recalculate
// and redraw every time, because although that is the most likely use
// case, it is not the only one.
if element.core.HasImage() {
element.redoAll()
element.core.DamageAll()
}
}
// Disown removes the given child from the container if it is contained within
// it.
func (element *Container) Disown (child tomo.Element) {
for index, entry := range element.children {
if entry.Element == child {
element.clearChildEventHandlers(entry.Element)
element.children = append (
element.children[:index],
element.children[index + 1:]...)
break
}
}
element.updateMinimumSize()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
func (element *Container) clearChildEventHandlers (child tomo.Element) {
child.DrawTo(nil, image.Rectangle { }, nil)
child.SetParent(nil)
if child, ok := child.(tomo.Focusable); ok {
if child.Focused() {
child.HandleUnfocus()
}
}
}
// DisownAll removes all child elements from the container at once.
func (element *Container) DisownAll () {
for _, entry := range element.children {
element.clearChildEventHandlers(entry.Element)
}
element.children = nil
element.updateMinimumSize()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
// Children returns a slice containing this element's children.
func (element *Container) Children () (children []tomo.Element) {
children = make([]tomo.Element, len(element.children))
for index, entry := range element.children {
children[index] = entry.Element
}
return
}
// CountChildren returns the amount of children contained within this element.
func (element *Container) CountChildren () (count int) {
return len(element.children)
}
// Child returns the child at the specified index. If the index is out of
// bounds, this method will return nil.
func (element *Container) Child (index int) (child tomo.Element) {
if index < 0 || index > len(element.children) { return }
return element.children[index].Element
}
// ChildAt returns the child that contains the specified x and y coordinates. If
// there are no children at the coordinates, this method will return nil.
func (element *Container) ChildAt (point image.Point) (child tomo.Element) {
for _, entry := range element.children {
if point.In(entry.Bounds) {
child = entry.Element
}
}
return
}
func (element *Container) redoAll () {
if !element.core.HasImage() { return }
// remove child canvasses so that any operations done in here will not
// cause a child to draw to a wack ass canvas.
for _, entry := range element.children {
entry.DrawTo(nil, entry.Bounds, nil)
}
// do a layout
element.doLayout()
// draw a background
rocks := make([]image.Rectangle, len(element.children))
for index, entry := range element.children {
rocks[index] = entry.Bounds
}
element.core.DrawBackgroundBoundsShatter (
element.theme.Pattern(tomo.PatternBackground, tomo.State { }),
element.Bounds(),
rocks...)
// cut our canvas up and give peices to child elements
for _, entry := range element.children {
entry.DrawTo (
canvas.Cut(element.core, entry.Bounds),
entry.Bounds, func (region image.Rectangle) {
element.core.DamageRegion(region)
})
}
}
func (element *Container) Window () tomo.Window {
return element.core.Window()
}
// NotifyMinimumSizeChange notifies the container that the minimum size of a
// child element has changed.
func (element *Container) NotifyMinimumSizeChange (child tomo.Element) {
element.updateMinimumSize()
element.redoAll()
element.core.DamageAll()
}
// DrawBackground draws a portion of the container's background pattern within
// the specified bounds. The container will not push these changes.
func (element *Container) DrawBackground (bounds image.Rectangle) {
element.core.DrawBackgroundBounds (
element.theme.Pattern(tomo.PatternBackground, tomo.State { }),
bounds)
}
// SetTheme sets the element's theme.
func (element *Container) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.Propagator.SetTheme(new)
element.updateMinimumSize()
element.redoAll()
}
// SetConfig sets the element's configuration.
func (element *Container) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.Propagator.SetConfig(new)
element.updateMinimumSize()
element.redoAll()
}
func (element *Container) updateMinimumSize () {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
width, height := element.layout.MinimumSize (
element.children, margin, padding)
element.core.SetMinimumSize(width, height)
}
func (element *Container) doLayout () {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
element.layout.Arrange (
element.children, margin,
padding, element.Bounds())
}

View File

@ -1,368 +0,0 @@
package containers
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/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// DocumentContainer is a scrollable container capable of containing flexible
// elements.
type DocumentContainer struct {
*core.Core
*core.Propagator
core core.CoreControl
children []tomo.LayoutEntry
scroll image.Point
warping bool
contentBounds image.Rectangle
config config.Wrapped
theme theme.Wrapped
onScrollBoundsChange func ()
}
// NewDocumentContainer creates a new document container.
func NewDocumentContainer () (element *DocumentContainer) {
element = &DocumentContainer { }
element.theme.Case = tomo.C("tomo", "documentContainer")
element.Core, element.core = core.NewCore(element, element.redoAll)
element.Propagator = core.NewPropagator(element, element.core)
return
}
// Adopt adds a new child element to the container. If expand is true, then the
// element will stretch to either side of the container (much like a css block
// element). If expand is false, the element will share a line with other inline
// elements.
func (element *DocumentContainer) Adopt (child tomo.Element, expand bool) {
// set event handlers
if child0, ok := child.(tomo.Themeable); ok {
child0.SetTheme(element.theme.Theme)
}
if child0, ok := child.(tomo.Configurable); ok {
child0.SetConfig(element.config.Config)
}
// add child
element.children = append (element.children, tomo.LayoutEntry {
Element: child,
Expand: expand,
})
child.SetParent(element)
// refresh stale data
element.updateMinimumSize()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
// Warp runs the specified callback, deferring all layout and rendering updates
// until the callback has finished executing. This allows for aplications to
// perform batch gui updates without flickering and stuff.
func (element *DocumentContainer) Warp (callback func ()) {
if element.warping {
callback()
return
}
element.warping = true
callback()
element.warping = false
if element.core.HasImage() {
element.redoAll()
element.core.DamageAll()
}
}
// Disown removes the given child from the container if it is contained within
// it.
func (element *DocumentContainer) Disown (child tomo.Element) {
for index, entry := range element.children {
if entry.Element == child {
element.clearChildEventHandlers(entry.Element)
element.children = append (
element.children[:index],
element.children[index + 1:]...)
break
}
}
element.updateMinimumSize()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
func (element *DocumentContainer) clearChildEventHandlers (child tomo.Element) {
child.DrawTo(nil, image.Rectangle { }, nil)
child.SetParent(nil)
if child, ok := child.(tomo.Focusable); ok {
if child.Focused() {
child.HandleUnfocus()
}
}
}
// DisownAll removes all child elements from the container at once.
func (element *DocumentContainer) DisownAll () {
for _, entry := range element.children {
element.clearChildEventHandlers(entry.Element)
}
element.children = nil
element.updateMinimumSize()
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
// Children returns a slice containing this element's children.
func (element *DocumentContainer) Children () (children []tomo.Element) {
children = make([]tomo.Element, len(element.children))
for index, entry := range element.children {
children[index] = entry.Element
}
return
}
// CountChildren returns the amount of children contained within this element.
func (element *DocumentContainer) CountChildren () (count int) {
return len(element.children)
}
// Child returns the child at the specified index. If the index is out of
// bounds, this method will return nil.
func (element *DocumentContainer) Child (index int) (child tomo.Element) {
if index < 0 || index > len(element.children) { return }
return element.children[index].Element
}
// ChildAt returns the child that contains the specified x and y coordinates. If
// there are no children at the coordinates, this method will return nil.
func (element *DocumentContainer) ChildAt (point image.Point) (child tomo.Element) {
for _, entry := range element.children {
if point.In(entry.Bounds) {
child = entry.Element
}
}
return
}
func (element *DocumentContainer) redoAll () {
if !element.core.HasImage() { return }
// do a layout
element.doLayout()
maxScrollHeight := element.maxScrollHeight()
if element.scroll.Y > maxScrollHeight {
element.scroll.Y = maxScrollHeight
element.doLayout()
}
// draw a background
rocks := make([]image.Rectangle, len(element.children))
for index, entry := range element.children {
rocks[index] = entry.Bounds
}
pattern := element.theme.Pattern (
tomo.PatternBackground,
tomo.State { })
artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...)
element.partition()
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
parent.NotifyScrollBoundsChange(element)
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *DocumentContainer) partition () {
for _, entry := range element.children {
entry.DrawTo(nil, entry.Bounds, nil)
}
// cut our canvas up and give peices to child elements
for _, entry := range element.children {
if entry.Bounds.Overlaps(element.Bounds()) {
entry.DrawTo (
canvas.Cut(element.core, entry.Bounds),
entry.Bounds, func (region image.Rectangle) {
element.core.DamageRegion(region)
})
}
}
}
func (element *DocumentContainer) Window () tomo.Window {
return element.core.Window()
}
// NotifyMinimumSizeChange notifies the container that the minimum size of a
// child element has changed.
func (element *DocumentContainer) NotifyMinimumSizeChange (child tomo.Element) {
element.redoAll()
element.core.DamageAll()
}
// DrawBackground draws a portion of the container's background pattern within
// the specified bounds. The container will not push these changes.
func (element *DocumentContainer) DrawBackground (bounds image.Rectangle) {
element.core.DrawBackgroundBounds (
element.theme.Pattern(tomo.PatternBackground, tomo.State { }),
bounds)
}
// NotifyFlexibleHeightChange notifies the parent that the parameters
// affecting a child's flexible height have changed. This method is
// expected to be called by flexible child element when their content
// changes.
func (element *DocumentContainer) NotifyFlexibleHeightChange (child tomo.Flexible) {
element.redoAll()
element.core.DamageAll()
}
// SetTheme sets the element's theme.
func (element *DocumentContainer) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.Propagator.SetTheme(new)
element.redoAll()
}
// SetConfig sets the element's configuration.
func (element *DocumentContainer) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.Propagator.SetConfig(new)
element.redoAll()
}
// ScrollContentBounds returns the full content size of the element.
func (element *DocumentContainer) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *DocumentContainer) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternBackground)
bounds := padding.Apply(element.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *DocumentContainer) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scroll = position
if element.core.HasImage() && !element.warping {
element.redoAll()
element.core.DamageAll()
}
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *DocumentContainer) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
func (element *DocumentContainer) maxScrollHeight () (height int) {
padding := element.theme.Padding(tomo.PatternSunken)
viewportHeight := element.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
// ScrollAxes returns the supported axes for scrolling.
func (element *DocumentContainer) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *DocumentContainer) doLayout () {
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
bounds := padding.Apply(element.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
xStart := dot.X
rowHeight := 0
nextLine := func () {
dot.X = xStart
dot.Y += margin.Y
dot.Y += rowHeight
rowHeight = 0
}
for index, entry := range element.children {
if dot.X > xStart && entry.Expand {
nextLine()
}
width, height := entry.MinimumSize()
if width + dot.X > bounds.Dx() && !entry.Expand {
nextLine()
}
if width < bounds.Dx() && entry.Expand {
width = bounds.Dx()
}
if typedChild, ok := entry.Element.(tomo.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
rowHeight = height
}
entry.Bounds.Min = dot
entry.Bounds.Max = image.Pt(dot.X + width, dot.Y + height)
element.children[index] = entry
element.contentBounds = element.contentBounds.Union(entry.Bounds)
if entry.Expand {
nextLine()
} else {
dot.X += width + margin.X
}
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
}
func (element *DocumentContainer) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternBackground)
minimumWidth := 0
for _, entry := range element.children {
width, _ := entry.MinimumSize()
if width > minimumWidth {
minimumWidth = width
}
}
element.core.SetMinimumSize (
minimumWidth + padding.Horizontal(),
padding.Vertical())
}

View File

@ -1,332 +0,0 @@
package containers
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// ScrollContainer is a container that is capable of holding a scrollable
// element.
type ScrollContainer struct {
*core.Core
*core.Propagator
core core.CoreControl
child tomo.Scrollable
horizontal *elements.ScrollBar
vertical *elements.ScrollBar
config config.Wrapped
theme theme.Wrapped
onFocusRequest func () (granted bool)
onFocusMotionRequest func (input.KeynavDirection) (granted bool)
}
// NewScrollContainer creates a new scroll container with the specified scroll
// bars.
func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) {
element = &ScrollContainer { }
element.theme.Case = tomo.C("tomo", "scrollContainer")
element.Core, element.core = core.NewCore(element, element.redoAll)
element.Propagator = core.NewPropagator(element, element.core)
if horizontal {
element.horizontal = elements.NewScrollBar(false)
element.setUpChild(element.horizontal)
element.horizontal.OnScroll (func (viewport image.Point) {
if element.child != nil {
element.child.ScrollTo(viewport)
}
if element.vertical != nil {
element.vertical.SetBounds (
element.child.ScrollContentBounds(),
element.child.ScrollViewportBounds())
}
})
}
if vertical {
element.vertical = elements.NewScrollBar(true)
element.setUpChild(element.vertical)
element.vertical.OnScroll (func (viewport image.Point) {
if element.child != nil {
element.child.ScrollTo(viewport)
}
if element.horizontal != nil {
element.horizontal.SetBounds (
element.child.ScrollContentBounds(),
element.child.ScrollViewportBounds())
}
})
}
return
}
// Adopt adds a scrollable element to the scroll container. The container can
// only contain one scrollable element at a time, and when a new one is adopted
// it replaces the last one.
func (element *ScrollContainer) Adopt (child tomo.Scrollable) {
// disown previous child if it exists
if element.child != nil {
element.disownChild(child)
}
// adopt new child
element.child = child
if child != nil {
element.setUpChild(child)
}
element.updateEnabled()
element.updateMinimumSize()
if element.core.HasImage() {
element.redoAll()
element.core.DamageAll()
}
}
func (element *ScrollContainer) setUpChild (child tomo.Element) {
child.SetParent(element)
if child, ok := child.(tomo.Themeable); ok {
child.SetTheme(element.theme.Theme)
}
if child, ok := child.(tomo.Configurable); ok {
child.SetConfig(element.config.Config)
}
}
func (element *ScrollContainer) disownChild (child tomo.Scrollable) {
child.DrawTo(nil, image.Rectangle { }, nil)
child.SetParent(nil)
if child, ok := child.(tomo.Focusable); ok {
if child.Focused() {
child.HandleUnfocus()
}
}
}
func (element *ScrollContainer) Window () tomo.Window {
return element.core.Window()
}
// NotifyMinimumSizeChange notifies the container that the minimum size of a
// child element has changed.
func (element *ScrollContainer) NotifyMinimumSizeChange (child tomo.Element) {
element.redoAll()
element.core.DamageAll()
}
// NotifyScrollBoundsChange notifies the container that the scroll bounds or
// axes of a child have changed.
func (element *ScrollContainer) NotifyScrollBoundsChange (child tomo.Scrollable) {
element.updateEnabled()
viewportBounds := element.child.ScrollViewportBounds()
contentBounds := element.child.ScrollContentBounds()
if element.horizontal != nil {
element.horizontal.SetBounds(contentBounds, viewportBounds)
}
if element.vertical != nil {
element.vertical.SetBounds(contentBounds, viewportBounds)
}
}
// DrawBackground draws a portion of the container's background pattern within
// the specified bounds. The container will not push these changes.
func (element *ScrollContainer) DrawBackground (bounds image.Rectangle) {
element.core.DrawBackgroundBounds (
element.theme.Pattern(tomo.PatternBackground, tomo.State { }),
bounds)
}
// SetTheme sets the element's theme.
func (element *ScrollContainer) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.Propagator.SetTheme(new)
element.updateMinimumSize()
element.redoAll()
}
// SetConfig sets the element's configuration.
func (element *ScrollContainer) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.Propagator.SetConfig(new)
element.updateMinimumSize()
element.redoAll()
}
func (element *ScrollContainer) HandleScroll (
x, y int,
deltaX, deltaY float64,
) {
horizontal, vertical := element.child.ScrollAxes()
if !horizontal { deltaX = 0 }
if !vertical { deltaY = 0 }
element.scrollChildBy(int(deltaX), int(deltaY))
}
// HandleKeyDown is called when a key is pressed down or repeated while
// this element has keyboard focus. It is important to note that not
// every key down event is guaranteed to be paired with exactly one key
// up event. This is the reason a list of modifier keys held down at the
// time of the key press is given.
func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
switch key {
case input.KeyPageUp:
viewport := element.child.ScrollViewportBounds()
element.HandleScroll(0, 0, 0, float64(-viewport.Dy()))
case input.KeyPageDown:
viewport := element.child.ScrollViewportBounds()
element.HandleScroll(0, 0, 0, float64(viewport.Dy()))
default:
element.Propagator.HandleKeyDown(key, modifiers)
}
}
// HandleKeyUp is called when a key is released while this element has
// keyboard focus.
func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modifiers) { }
// CountChildren returns the amount of children contained within this element.
func (element *ScrollContainer) CountChildren () (count int) {
return 3
}
// Child returns the child at the specified index. If the index is out of
// bounds, this method will return nil.
func (element *ScrollContainer) Child (index int) (child tomo.Element) {
switch index {
case 0: return element.child
case 1:
if element.horizontal == nil {
return nil
} else {
return element.horizontal
}
case 2:
if element.vertical == nil {
return nil
} else {
return element.vertical
}
default: return nil
}
}
func (element *ScrollContainer) redoAll () {
if !element.core.HasImage() { return }
zr := image.Rectangle { }
if element.child != nil { element.child.DrawTo(nil, zr, nil) }
if element.horizontal != nil { element.horizontal.DrawTo(nil, zr, nil) }
if element.vertical != nil { element.vertical.DrawTo(nil, zr, nil) }
childBounds, horizontalBounds, verticalBounds := element.layout()
if element.child != nil {
element.child.DrawTo (
canvas.Cut(element.core, childBounds),
childBounds, element.childDamageCallback)
}
if element.horizontal != nil {
element.horizontal.DrawTo (
canvas.Cut(element.core, horizontalBounds),
horizontalBounds, element.childDamageCallback)
}
if element.vertical != nil {
element.vertical.DrawTo (
canvas.Cut(element.core, verticalBounds),
verticalBounds, element.childDamageCallback)
}
element.draw()
}
func (element *ScrollContainer) scrollChildBy (x, y int) {
if element.child == nil { return }
scrollPoint :=
element.child.ScrollViewportBounds().Min.
Add(image.Pt(x, y))
element.child.ScrollTo(scrollPoint)
}
func (element *ScrollContainer) childDamageCallback (region image.Rectangle) {
element.core.DamageRegion(region)
}
func (element *ScrollContainer) layout () (
child image.Rectangle,
horizontal image.Rectangle,
vertical image.Rectangle,
) {
bounds := element.Bounds()
child = bounds
if element.horizontal != nil {
_, hMinHeight := element.horizontal.MinimumSize()
child.Max.Y -= hMinHeight
}
if element.vertical != nil {
vMinWidth, _ := element.vertical.MinimumSize()
child.Max.X -= vMinWidth
}
vertical.Min.X = child.Max.X
vertical.Max.X = bounds.Max.X
vertical.Min.Y = bounds.Min.Y
vertical.Max.Y = child.Max.Y
horizontal.Min.X = bounds.Min.X
horizontal.Max.X = child.Max.X
horizontal.Min.Y = child.Max.Y
horizontal.Max.Y = bounds.Max.Y
return
}
func (element *ScrollContainer) draw () {
if element.horizontal != nil && element.vertical != nil {
bounds := element.Bounds()
bounds.Min = image.Pt (
bounds.Max.X - element.vertical.Bounds().Dx(),
bounds.Max.Y - element.horizontal.Bounds().Dy())
state := tomo.State { }
deadArea := element.theme.Pattern(tomo.PatternDead, state)
deadArea.Draw(canvas.Cut(element.core, bounds), bounds)
}
}
func (element *ScrollContainer) updateMinimumSize () {
var width, height int
if element.child != nil {
width, height = element.child.MinimumSize()
}
if element.horizontal != nil {
hMinWidth, hMinHeight := element.horizontal.MinimumSize()
height += hMinHeight
if hMinWidth > width {
width = hMinWidth
}
}
if element.vertical != nil {
vMinWidth, vMinHeight := element.vertical.MinimumSize()
width += vMinWidth
if vMinHeight > height {
height = vMinHeight
}
}
element.core.SetMinimumSize(width, height)
}
func (element *ScrollContainer) updateEnabled () {
horizontal, vertical := element.child.ScrollAxes()
if element.horizontal != nil {
element.horizontal.SetEnabled(horizontal)
}
if element.vertical != nil {
element.vertical.SetEnabled(vertical)
}
}

View File

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

View File

@ -1,235 +0,0 @@
package core
import "image"
import "image/color"
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/shatter"
// Core is a struct that implements some core functionality common to most
// widgets. It is meant to be embedded directly into a struct.
type Core struct {
canvas canvas.Canvas
bounds image.Rectangle
parent tomo.Parent
outer tomo.Element
metrics struct {
minimumWidth int
minimumHeight int
}
drawSizeChange func ()
onDamage func (region image.Rectangle)
}
// NewCore creates a new element core and its corresponding control given the
// element that it will be a part of. If outer is nil, this function will return
// nil.
func NewCore (
outer tomo.Element,
drawSizeChange func (),
) (
core *Core,
control CoreControl,
) {
if outer == nil { return }
core = &Core {
outer: outer,
drawSizeChange: drawSizeChange,
}
control = CoreControl { core: core }
return
}
// Bounds fulfills the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) Bounds () (bounds image.Rectangle) {
if core.canvas == nil { return }
return core.bounds
}
// MinimumSize fulfils the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) MinimumSize () (width, height int) {
return core.metrics.minimumWidth, core.metrics.minimumHeight
}
// MinimumSize fulfils the tomo.Element interface. This should not need to be
// overridden, unless you want to detect when the element is parented or
// unparented.
func (core *Core) SetParent (parent tomo.Parent) {
if parent != nil && core.parent != nil {
panic("core.SetParent: element already has a parent")
}
core.parent = parent
}
// DrawTo fulfills the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) DrawTo (
canvas canvas.Canvas,
bounds image.Rectangle,
onDamage func (region image.Rectangle),
) {
core.canvas = canvas
core.bounds = bounds
core.onDamage = onDamage
if core.drawSizeChange != nil && core.canvas != nil {
core.drawSizeChange()
}
}
// CoreControl is a struct that can exert control over a Core struct. It can be
// used as a canvas. It must not be directly embedded into an element, but
// instead kept as a private member. When a Core struct is created, a
// corresponding CoreControl struct is linked to it and returned alongside it.
type CoreControl struct {
core *Core
}
// ColorModel fulfills the draw.Image interface.
func (control CoreControl) ColorModel () (model color.Model) {
return color.RGBAModel
}
// At fulfills the draw.Image interface.
func (control CoreControl) At (x, y int) (pixel color.Color) {
if control.core.canvas == nil { return }
return control.core.canvas.At(x, y)
}
// Bounds fulfills the draw.Image interface.
func (control CoreControl) Bounds () (bounds image.Rectangle) {
if control.core.canvas == nil { return }
return control.core.canvas.Bounds()
}
// Set fulfills the draw.Image interface.
func (control CoreControl) Set (x, y int, c color.Color) () {
if control.core.canvas == nil { return }
control.core.canvas.Set(x, y, c)
}
// Buffer fulfills the canvas.Canvas interface.
func (control CoreControl) Buffer () (data []color.RGBA, stride int) {
if control.core.canvas == nil { return }
return control.core.canvas.Buffer()
}
// Parent returns the element's parent.
func (control CoreControl) Parent () tomo.Parent {
return control.core.parent
}
// DrawBackground fills the element's canvas with the parent's background
// pattern, if the parent supports it. If it is not supported, the fallback
// pattern will be used instead.
func (control CoreControl) DrawBackground (fallback artist.Pattern) {
control.DrawBackgroundBounds(fallback, control.Bounds())
}
// DrawBackgroundBounds is like DrawBackground, but it takes in a bounding
// rectangle instead of using the element's bounds.
func (control CoreControl) DrawBackgroundBounds (
fallback artist.Pattern,
bounds image.Rectangle,
) {
parent, ok := control.Parent().(tomo.BackgroundParent)
if ok {
parent.DrawBackground(bounds)
} else if fallback != nil {
fallback.Draw(canvas.Cut(control, bounds), control.Bounds())
}
}
// DrawBackgroundBoundsShatter is like DrawBackgroundBounds, but uses the
// shattering algorithm to avoid drawing in areas specified by rocks.
func (control CoreControl) DrawBackgroundBoundsShatter (
fallback artist.Pattern,
bounds image.Rectangle,
rocks ...image.Rectangle,
) {
tiles := shatter.Shatter(bounds, rocks...)
for _, tile := range tiles {
control.DrawBackgroundBounds(fallback, tile)
}
}
// Window returns the window containing the element.
func (control CoreControl) Window () tomo.Window {
parent := control.Parent()
if parent == nil {
return nil
} else {
return parent.Window()
}
}
// Outer returns the outer element given when the control was constructed.
func (control CoreControl) Outer () tomo.Element {
return control.core.outer
}
// HasImage returns true if the core has an allocated image buffer, and false if
// it doesn't.
func (control CoreControl) HasImage () (has bool) {
return control.core.canvas != nil && !control.core.canvas.Bounds().Empty()
}
// DamageRegion pushes the selected region of pixels to the parent element. This
// does not need to be called when responding to a resize event.
func (control CoreControl) DamageRegion (regions ...image.Rectangle) {
if control.core.canvas == nil { return }
if control.core.onDamage != nil {
for _, region := range regions {
control.core.onDamage(region)
}
}
}
// DamageAll pushes all pixels to the parent element. This does not need to be
// called when redrawing in response to a change in size.
func (control CoreControl) DamageAll () {
control.DamageRegion(control.core.Bounds())
}
// SetMinimumSize sets the minimum size of this element, notifying the parent
// element in the process.
func (control CoreControl) SetMinimumSize (width, height int) {
core := control.core
if width == core.metrics.minimumWidth &&
height == core.metrics.minimumHeight {
return
}
core.metrics.minimumWidth = width
core.metrics.minimumHeight = height
if control.core.parent != nil {
control.core.parent.NotifyMinimumSizeChange(control.core.outer)
}
}
// ConstrainSize contstrains the specified width and height to the minimum width
// and height, and returns wether or not anything ended up being constrained.
func (control CoreControl) ConstrainSize (
inWidth, inHeight int,
) (
outWidth, outHeight int,
constrained bool,
) {
core := control.core
outWidth = inWidth
outHeight = inHeight
if outWidth < core.metrics.minimumWidth {
outWidth = core.metrics.minimumWidth
constrained = true
}
if outHeight < core.metrics.minimumHeight {
outHeight = core.metrics.minimumHeight
constrained = true
}
return
}

View File

@ -1,7 +0,0 @@
// Package core provides tools that allow elements to easily fulfill common
// interfaces without having to duplicate a ton of code. Each "core" is a type
// that can be embedded into an element directly, working to fulfill a
// particular interface. Each one comes with a corresponding core control, which
// provides an interface for elements to exert control over the core. Core
// controls should be kept private.
package core

View File

@ -1,101 +0,0 @@
package core
// import "runtime/debug"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
// FocusableCore is a struct that can be embedded into objects to make them
// focusable, giving them the default keynav behavior.
type FocusableCore struct {
core CoreControl
focused bool
enabled bool
drawFocusChange func ()
}
// NewFocusableCore creates a new focusability core and its corresponding
// control. If your element needs to visually update itself when it's focus
// state changes (which it should), a callback to draw and push the update can
// be specified.
func NewFocusableCore (
core CoreControl,
drawFocusChange func (),
) (
focusable *FocusableCore,
control FocusableCoreControl,
) {
focusable = &FocusableCore {
core: core,
drawFocusChange: drawFocusChange,
enabled: true,
}
control = FocusableCoreControl { core: focusable }
return
}
// Focused returns whether or not this element is currently focused.
func (core *FocusableCore) Focused () (focused bool) {
return core.focused
}
// Focus focuses this element, if its parent element grants the request.
func (core *FocusableCore) Focus () {
if !core.enabled || core.focused { return }
parent := core.core.Parent()
if parent, ok := parent.(tomo.FocusableParent); ok {
core.focused = parent.RequestFocus (
core.core.Outer().(tomo.Focusable))
}
}
// HandleFocus causes this element to mark itself as focused, if it can
// currently be. Otherwise, it will return false and do nothing.
func (core *FocusableCore) HandleFocus (
direction input.KeynavDirection,
) (
accepted bool,
) {
direction = direction.Canon()
if !core.enabled { return false }
if core.focused && direction != input.KeynavDirectionNeutral {
return false
}
if core.focused == false {
core.focused = true
if core.drawFocusChange != nil { core.drawFocusChange() }
}
return true
}
// HandleUnfocus causes this element to mark itself as unfocused.
func (core *FocusableCore) HandleUnfocus () {
core.focused = false
// debug.PrintStack()
if core.drawFocusChange != nil { core.drawFocusChange() }
}
// Enabled returns whether or not the element is enabled.
func (core *FocusableCore) Enabled () (enabled bool) {
return core.enabled
}
// FocusableCoreControl is a struct that can be used to exert control over a
// focusability core. It must not be directly embedded into an element, but
// instead kept as a private member. When a FocusableCore struct is created, a
// corresponding FocusableCoreControl struct is linked to it and returned
// alongside it.
type FocusableCoreControl struct {
core *FocusableCore
}
// SetEnabled sets whether the focusability core is enabled. If the state
// changes, this will call drawFocusChange.
func (control FocusableCoreControl) SetEnabled (enabled bool) {
if control.core.enabled == enabled { return }
control.core.enabled = enabled
if !enabled { control.core.focused = false }
if control.core.drawFocusChange != nil {
control.core.drawFocusChange()
}
}

View File

@ -1,355 +0,0 @@
package core
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
// Container represents an object that can provide access to a list of child
// elements.
type Container interface {
Child (index int) tomo.Element
CountChildren () int
}
// Propagator is a struct that can be embedded into elements that contain one or
// more children in order to propagate events to them without having to write
// all of the event handlers. It also implements standard behavior for focus
// propagation and keyboard navigation.
type Propagator struct {
core CoreControl
container Container
drags [10]tomo.MouseTarget
focused bool
}
// NewPropagator creates a new event propagator that uses the specified
// container to access a list of child elements that will have events propagated
// to them. If container is nil, the function will return nil.
func NewPropagator (container Container, core CoreControl) (propagator *Propagator) {
if container == nil { return nil }
propagator = &Propagator {
core: core,
container: container,
}
return
}
// ----------- Interface fulfillment methods ----------- //
// Focused returns whether or not this element or any of its children
// are currently focused.
func (propagator *Propagator) Focused () (focused bool) {
return propagator.focused
}
// Focus focuses this element, if its parent element grants the
// request.
func (propagator *Propagator) Focus () {
if propagator.focused == true { return }
parent := propagator.core.Parent()
if parent, ok := parent.(tomo.FocusableParent); ok && parent != nil {
propagator.focused = parent.RequestFocus (
propagator.core.Outer().(tomo.Focusable))
}
}
// HandleFocus causes this element to mark itself as focused. If the
// element does not have children or there are no more focusable children in
// the given direction, it should return false and do nothing. Otherwise, it
// marks itself as focused along with any applicable children and returns
// true.
func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (accepted bool) {
direction = direction.Canon()
firstFocused := propagator.firstFocused()
if firstFocused < 0 {
// no element is currently focused, so we need to focus either
// the first or last focusable element depending on the
// direction.
switch direction {
case input.KeynavDirectionForward:
// if we recieve a forward direction, focus the first
// focusable element.
return propagator.focusFirstFocusableElement(direction)
case input.KeynavDirectionBackward:
// if we recieve a backward direction, focus the last
// focusable element.
return propagator.focusLastFocusableElement(direction)
case input.KeynavDirectionNeutral:
// if we recieve a neutral direction, just focus this
// element and nothing else.
propagator.focused = true
return true
}
} else {
// an element is currently focused, so we need to move the
// focus in the specified direction
firstFocusedChild :=
propagator.container.Child(firstFocused).
(tomo.Focusable)
// before we move the focus, the currently focused child
// may also be able to move its focus. if the child is able
// to do that, we will let it and not move ours.
if firstFocusedChild.HandleFocus(direction) {
return true
}
// find the previous/next focusable element relative to the
// currently focused element, if it exists.
for index := firstFocused + int(direction);
index < propagator.container.CountChildren() && index >= 0;
index += int(direction) {
child, focusable :=
propagator.container.Child(index).
(tomo.Focusable)
if focusable && child.HandleFocus(direction) {
// we have found one, so we now actually move
// the focus.
firstFocusedChild.HandleUnfocus()
propagator.focused = true
return true
}
}
}
return false
}
// RequestFocus notifies the parent that a child element is requesting
// keyboard focus. If the parent grants the request, the method will
// return true and the child element should behave as if a HandleFocus
// call was made.
func (propagator *Propagator) RequestFocus (
child tomo.Focusable,
) (
granted bool,
) {
if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok {
if parent.RequestFocus(propagator.core.Outer().(tomo.Focusable)) {
propagator.HandleUnfocus()
propagator.focused = true
granted = true
}
}
return
}
// RequestFocusMotion notifies the parent that a child element wants the
// focus to be moved to the next focusable element.
func (propagator *Propagator) RequestFocusNext (child tomo.Focusable) {
if !propagator.focused { return }
if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok {
parent.RequestFocusNext(propagator.core.Outer().(tomo.Focusable))
}
}
// RequestFocusMotion notifies the parent that a child element wants the
// focus to be moved to the previous focusable element.
func (propagator *Propagator) RequestFocusPrevious (child tomo.Focusable) {
if !propagator.focused { return }
if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok {
parent.RequestFocusPrevious(propagator.core.Outer().(tomo.Focusable))
}
}
// HandleDeselection causes this element to mark itself and all of its children
// as unfocused.
func (propagator *Propagator) HandleUnfocus () {
propagator.forFocusable (func (child tomo.Focusable) bool {
child.HandleUnfocus()
return true
})
propagator.focused = false
}
// HandleKeyDown propogates the keyboard event to the currently selected child.
func (propagator *Propagator) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
propagator.forFocused (func (child tomo.Focusable) bool {
typedChild, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard {
typedChild.HandleKeyDown(key, modifiers)
}
return true
})
}
// HandleKeyUp propogates the keyboard event to the currently selected child.
func (propagator *Propagator) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
propagator.forFocused (func (child tomo.Focusable) bool {
typedChild, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard {
typedChild.HandleKeyUp(key, modifiers)
}
return true
})
}
// HandleMouseDown propagates the mouse event to the element under the mouse
// pointer.
func (propagator *Propagator) HandleMouseDown (x, y int, button input.Button) {
child, handlesMouse :=
propagator.childAt(image.Pt(x, y)).
(tomo.MouseTarget)
if handlesMouse {
propagator.drags[button] = child
child.HandleMouseDown(x, y, button)
}
}
// HandleMouseUp propagates the mouse event to the element that the released
// mouse button was originally pressed on.
func (propagator *Propagator) HandleMouseUp (x, y int, button input.Button) {
child := propagator.drags[button]
if child != nil {
propagator.drags[button] = nil
child.HandleMouseUp(x, y, button)
}
}
// HandleMotion propagates the mouse event to the element that was last
// pressed down by the mouse if the mouse is currently being held down, else it
// propagates the event to whichever element is underneath the mouse pointer.
func (propagator *Propagator) HandleMotion (x, y int) {
handled := false
for _, child := range propagator.drags {
if child, ok := child.(tomo.MotionTarget); ok {
child.HandleMotion(x, y)
handled = true
}
}
if !handled {
child := propagator.childAt(image.Pt(x, y))
if child, ok := child.(tomo.MotionTarget); ok {
child.HandleMotion(x, y)
}
}
}
// HandleScroll propagates the mouse event to the element under the mouse
// pointer.
func (propagator *Propagator) HandleScroll (x, y int, deltaX, deltaY float64) {
child := propagator.childAt(image.Pt(x, y))
if child, ok := child.(tomo.ScrollTarget); ok {
child.HandleScroll(x, y, deltaX, deltaY)
}
}
// SetTheme sets the theme of all children to the specified theme.
func (propagator *Propagator) SetTheme (theme tomo.Theme) {
propagator.forChildren (func (child tomo.Element) bool {
typedChild, themeable := child.(tomo.Themeable)
if themeable {
typedChild.SetTheme(theme)
}
return true
})
}
// SetConfig sets the theme of all children to the specified config.
func (propagator *Propagator) SetConfig (config tomo.Config) {
propagator.forChildren (func (child tomo.Element) bool {
typedChild, configurable := child.(tomo.Configurable)
if configurable {
typedChild.SetConfig(config)
}
return true
})
}
// ----------- Focusing utilities ----------- //
func (propagator *Propagator) focusFirstFocusableElement (
direction input.KeynavDirection,
) (
ok bool,
) {
propagator.forFocusable (func (child tomo.Focusable) bool {
if child.HandleFocus(direction) {
propagator.focused = true
ok = true
return false
}
return true
})
return
}
func (propagator *Propagator) focusLastFocusableElement (
direction input.KeynavDirection,
) (
ok bool,
) {
propagator.forChildrenReverse (func (child tomo.Element) bool {
typedChild, focusable := child.(tomo.Focusable)
if focusable && typedChild.HandleFocus(direction) {
propagator.focused = true
ok = true
return false
}
return true
})
return
}
// ----------- Iterator utilities ----------- //
func (propagator *Propagator) forChildren (callback func (child tomo.Element) bool) {
for index := 0; index < propagator.container.CountChildren(); index ++ {
child := propagator.container.Child(index)
if child == nil { continue }
if !callback(child) { break }
}
}
func (propagator *Propagator) forChildrenReverse (callback func (child tomo.Element) bool) {
for index := propagator.container.CountChildren() - 1; index > 0; index -- {
child := propagator.container.Child(index)
if child == nil { continue }
if !callback(child) { break }
}
}
func (propagator *Propagator) childAt (position image.Point) (child tomo.Element) {
propagator.forChildren (func (current tomo.Element) bool {
if position.In(current.Bounds()) {
child = current
}
return true
})
return
}
func (propagator *Propagator) forFocused (callback func (child tomo.Focusable) bool) {
propagator.forChildren (func (child tomo.Element) bool {
typedChild, focusable := child.(tomo.Focusable)
if focusable && typedChild.Focused() {
if !callback(typedChild) { return false }
}
return true
})
}
func (propagator *Propagator) forFocusable (callback func (child tomo.Focusable) bool) {
propagator.forChildren (func (child tomo.Element) bool {
typedChild, focusable := child.(tomo.Focusable)
if focusable {
if !callback(typedChild) { return false }
}
return true
})
}
func (propagator *Propagator) firstFocused () int {
for index := 0; index < propagator.container.CountChildren(); index ++ {
child, focusable := propagator.container.Child(index).(tomo.Focusable)
if focusable && child.Focused() {
return index
}
}
return -1
}

312
elements/directory.go Normal file
View File

@ -0,0 +1,312 @@
package elements
import "image"
import "path/filepath"
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/shatter"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
// TODO: base on flow implementation of list. also be able to switch to a table
// variant for a more information dense view.
type directoryEntity interface {
tomo.ContainerEntity
tomo.ScrollableEntity
}
type historyEntry struct {
location string
filesystem ReadDirStatFS
}
// Directory displays a list of files within a particular directory and
// file system.
type Directory struct {
container
entity directoryEntity
theme theme.Wrapped
scroll image.Point
contentBounds image.Rectangle
history []historyEntry
historyIndex int
onChoose func (file string)
onScrollBoundsChange func ()
}
// NewDirectory creates a new directory view. If within is nil, it will use
// the OS file system.
func NewDirectory (
location string,
within ReadDirStatFS,
) (
element *Directory,
err error,
) {
element = &Directory { }
element.theme.Case = tomo.C("tomo", "list")
element.entity = tomo.NewEntity(element).(directoryEntity)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init()
err = element.SetLocation(location, within)
return
}
func (element *Directory) Draw (destination canvas.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.DrawBackground(canvas.Cut(destination, tile))
}
}
func (element *Directory) Layout () {
if element.scroll.Y > element.maxScrollHeight() {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.theme.Margin(tomo.PatternPinboard)
padding := element.theme.Padding(tomo.PatternPinboard)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
xStart := dot.X
rowHeight := 0
nextLine := func () {
dot.X = xStart
dot.Y += margin.Y
dot.Y += rowHeight
rowHeight = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
child := element.entity.Child(index)
entry := element.scratch[child]
width := int(entry.minBreadth)
height := int(entry.minSize)
if width + dot.X > bounds.Max.X {
nextLine()
}
if typedChild, ok := child.(tomo.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
rowHeight = height
}
childBounds := tomo.Bounds (
dot.X, dot.Y,
width, height)
element.entity.PlaceChild(index, childBounds)
element.contentBounds = element.contentBounds.Union(childBounds)
dot.X += width + margin.X
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
element.entity.NotifyScrollBoundsChange()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *Directory) HandleMouseDown (x, y int, button input.Button) {
element.selectNone()
}
func (element *Directory) HandleMouseUp (x, y int, button input.Button) { }
func (element *Directory) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) {
element.selectNone()
if child, ok := child.(tomo.Selectable); ok {
index := element.entity.IndexOf(child)
element.entity.SelectChild(index, true)
}
}
func (element *Directory) HandleChildMouseUp (int, int, input.Button, tomo.Element) { }
func (element *Directory) HandleChildFlexibleHeightChange (child tomo.Flexible) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// ScrollContentBounds returns the full content size of the element.
func (element *Directory) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *Directory) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternPinboard)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *Directory) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scroll = position
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *Directory) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// ScrollAxes returns the supported axes for scrolling.
func (element *Directory) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *Directory) DrawBackground (destination canvas.Canvas) {
element.theme.Pattern(tomo.PatternPinboard, tomo.State { }).
Draw(destination, element.entity.Bounds())
}
// SetTheme sets the element's theme.
func (element *Directory) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// Location returns the directory's location and filesystem.
func (element *Directory) Location () (string, ReadDirStatFS) {
if len(element.history) < 1 { return "", nil }
current := element.history[element.historyIndex]
return current.location, current.filesystem
}
// SetLocation sets the directory's location and filesystem. If within is nil,
// it will use the OS file system.
func (element *Directory) SetLocation (
location string,
within ReadDirStatFS,
) error {
if within == nil {
within = defaultFS { }
}
element.scroll = image.Point { }
if element.history != nil {
element.historyIndex ++
}
element.history = append (
element.history[:element.historyIndex],
historyEntry { location, within })
return element.Update()
}
// Backward goes back a directory in history
func (element *Directory) Backward () (bool, error) {
if element.historyIndex > 1 {
element.historyIndex --
return true, element.Update()
} else {
return false, nil
}
}
// Forward goes forward a directory in history
func (element *Directory) Forward () (bool, error) {
if element.historyIndex < len(element.history) - 1 {
element.historyIndex ++
return true, element.Update()
} else {
return false, nil
}
}
// Update refreshes the directory's contents.
func (element *Directory) Update () error {
location, filesystem := element.Location()
entries, err := filesystem.ReadDir(location)
children := make([]tomo.Element, len(entries))
for index, entry := range entries {
filePath := filepath.Join(location, entry.Name())
file, _ := NewFile(filePath, filesystem)
file.OnChoose (func () {
if element.onChoose != nil {
element.onChoose(filePath)
}
})
children[index] = file
}
element.DisownAll()
element.Adopt(children...)
return err
}
// OnChoose sets a function to be called when the user double-clicks a file or
// sub-directory within the directory view.
func (element *Directory) OnChoose (callback func (file string)) {
element.onChoose = callback
}
func (element *Directory) selectNone () {
for index := 0; index < element.entity.CountChildren(); index ++ {
element.entity.SelectChild(index, false)
}
}
func (element *Directory) maxScrollHeight () (height int) {
padding := element.theme.Padding(tomo.PatternSunken)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
func (element *Directory) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternPinboard)
minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(index)
if width > minimumWidth {
minimumWidth = width
}
key := element.entity.Child(index)
entry := element.scratch[key]
entry.minSize = float64(height)
entry.minBreadth = float64(width)
element.scratch[key] = entry
}
element.entity.SetMinimumSize (
minimumWidth + padding.Horizontal(),
padding.Vertical())
}

208
elements/document.go Normal file
View File

@ -0,0 +1,208 @@
package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/shatter"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
type documentEntity interface {
tomo.ContainerEntity
tomo.ScrollableEntity
}
type Document struct {
container
entity documentEntity
scroll image.Point
contentBounds image.Rectangle
theme theme.Wrapped
onScrollBoundsChange func ()
}
func NewDocument (children ...tomo.Element) (element *Document) {
element = &Document { }
element.theme.Case = tomo.C("tomo", "document")
element.entity = tomo.NewEntity(element).(documentEntity)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init()
element.Adopt(children...)
return
}
func (element *Document) Draw (destination canvas.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
for _, tile := range tiles {
element.entity.DrawBackground(canvas.Cut(destination, tile))
}
}
func (element *Document) Layout () {
if element.scroll.Y > element.maxScrollHeight() {
element.scroll.Y = element.maxScrollHeight()
}
margin := element.theme.Margin(tomo.PatternBackground)
padding := element.theme.Padding(tomo.PatternBackground)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
xStart := dot.X
rowHeight := 0
nextLine := func () {
dot.X = xStart
dot.Y += margin.Y
dot.Y += rowHeight
rowHeight = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
child := element.entity.Child(index)
entry := element.scratch[child]
if dot.X > xStart && entry.expand {
nextLine()
}
width := int(entry.minBreadth)
height := int(entry.minSize)
if width + dot.X > bounds.Max.X && !entry.expand {
nextLine()
}
if width < bounds.Dx() && entry.expand {
width = bounds.Dx()
}
if typedChild, ok := child.(tomo.Flexible); ok {
height = typedChild.FlexibleHeightFor(width)
}
if rowHeight < height {
rowHeight = height
}
childBounds := tomo.Bounds (
dot.X, dot.Y,
width, height)
element.entity.PlaceChild(index, childBounds)
element.contentBounds = element.contentBounds.Union(childBounds)
if entry.expand {
nextLine()
} else {
dot.X += width + margin.X
}
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
element.entity.NotifyScrollBoundsChange()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
func (element *Document) Adopt (children ...tomo.Element) {
element.adopt(true, children...)
}
func (element *Document) AdoptInline (children ...tomo.Element) {
element.adopt(false, children...)
}
func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Document) DrawBackground (destination canvas.Canvas) {
element.entity.DrawBackground(destination)
}
// SetTheme sets the element's theme.
func (element *Document) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// ScrollContentBounds returns the full content size of the element.
func (element *Document) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *Document) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternBackground)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *Document) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scroll = position
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *Document) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// ScrollAxes returns the supported axes for scrolling.
func (element *Document) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *Document) maxScrollHeight () (height int) {
padding := element.theme.Padding(tomo.PatternSunken)
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
func (element *Document) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternBackground)
minimumWidth := 0
for index := 0; index < element.entity.CountChildren(); index ++ {
width, height := element.entity.ChildMinimumSize(index)
if width > minimumWidth {
minimumWidth = width
}
key := element.entity.Child(index)
entry := element.scratch[key]
entry.minSize = float64(height)
entry.minBreadth = float64(width)
element.scratch[key] = entry
}
element.entity.SetMinimumSize (
minimumWidth + padding.Horizontal(),
padding.Vertical())
}

View File

@ -1,4 +1,4 @@
package fileElements
package elements
import "time"
import "io/fs"
@ -6,27 +6,29 @@ import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type fileEntity interface {
tomo.SelectableEntity
tomo.FocusableEntity
}
// File displays an interactive visual representation of a file within any
// file system.
type File struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
entity fileEntity
config config.Wrapped
theme theme.Wrapped
lastClick time.Time
pressed bool
enabled bool
iconID tomo.Icon
filesystem fs.StatFS
location string
selected bool
onChoose func ()
}
@ -40,15 +42,44 @@ func NewFile (
element *File,
err error,
) {
element = &File { }
element = &File { enabled: true }
element.theme.Case = tomo.C("files", "file")
element.Core, element.core = core.NewCore(element, element.drawAll)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush)
element.entity = tomo.NewEntity(element).(fileEntity)
err = element.SetLocation(location, within)
return
}
// Entity returns this element's entity.
func (element *File) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *File) Draw (destination canvas.Canvas) {
// background
state := element.state()
bounds := element.entity.Bounds()
sink := element.theme.Sink(tomo.PatternButton)
element.theme.
Pattern(tomo.PatternButton, state).
Draw(destination, bounds)
// icon
icon := element.icon()
if icon != nil {
iconBounds := icon.Bounds()
offset := image.Pt (
(bounds.Dx() - iconBounds.Dx()) / 2,
(bounds.Dy() - iconBounds.Dy()) / 2)
if element.pressed {
offset = offset.Add(sink)
}
icon.Draw (
destination,
element.theme.Color(tomo.ColorForeground, state),
bounds.Min.Add(offset))
}
}
// Location returns the file's location and filesystem.
func (element *File) Location () (string, fs.StatFS) {
return element.location, element.filesystem
@ -82,55 +113,70 @@ func (element *File) Update () error {
}
element.updateMinimumSize()
element.drawAndPush()
element.entity.Invalidate()
return err
}
func (element *File) Selected () bool {
return element.selected
}
func (element *File) SetSelected (selected bool) {
if element.selected == selected { return }
element.selected = selected
element.drawAndPush()
}
func (element *File) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if !element.Enabled() { return }
if key == input.KeyEnter {
element.pressed = true
element.drawAndPush()
element.entity.Invalidate()
}
}
func (element *File) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.drawAndPush()
if !element.Enabled() { return }
element.entity.Invalidate()
if element.onChoose != nil {
element.onChoose()
}
}
}
func (element *File) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *File) HandleSelectionChange () {
element.entity.Invalidate()
}
func (element *File) OnChoose (callback func ()) {
element.onChoose = callback
}
// Focus gives this element input focus.
func (element *File) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this file is enabled or not.
func (element *File) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this file is enabled or not.
func (element *File) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
func (element *File) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
if !element.entity.Focused() { element.Focus() }
if button != input.ButtonLeft { return }
element.pressed = true
element.drawAndPush()
element.entity.Invalidate()
}
func (element *File) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft { return }
element.pressed = false
within := image.Point { x, y }.In(element.Bounds())
within := image.Point { x, y }.In(element.entity.Bounds())
if time.Since(element.lastClick) < element.config.DoubleClickDelay() {
if element.Enabled() && within && element.onChoose != nil {
element.onChoose()
@ -138,29 +184,29 @@ func (element *File) HandleMouseUp (x, y int, button input.Button) {
} else {
element.lastClick = time.Now()
}
element.drawAndPush()
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *File) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawAndPush()
func (element *File) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *File) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.drawAndPush()
func (element *File) SetConfig (config tomo.Config) {
if config == element.config.Config { return }
element.config.Config = config
element.entity.Invalidate()
}
func (element *File) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
On: element.selected,
On: element.entity.Selected(),
}
}
@ -172,44 +218,11 @@ func (element *File) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternButton)
icon := element.icon()
if icon == nil {
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
padding.Horizontal(),
padding.Vertical())
} else {
bounds := padding.Inverse().Apply(icon.Bounds())
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
}
}
func (element *File) drawAndPush () {
if element.core.HasImage() {
element.drawAll()
element.core.DamageAll()
}
}
func (element *File) drawAll () {
// background
state := element.state()
bounds := element.Bounds()
sink := element.theme.Sink(tomo.PatternButton)
element.theme.
Pattern(tomo.PatternButton, state).
Draw(element.core, bounds)
// icon
icon := element.icon()
if icon != nil {
iconBounds := icon.Bounds()
offset := image.Pt (
(bounds.Dx() - iconBounds.Dx()) / 2,
(bounds.Dy() - iconBounds.Dy()) / 2)
if element.pressed {
offset = offset.Add(sink)
}
icon.Draw (
element.core,
element.theme.Color(tomo.ColorForeground, state),
bounds.Min.Add(offset))
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
}
}

View File

@ -1,378 +0,0 @@
package fileElements
import "io/fs"
import "image"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type fileLayoutEntry struct {
*File
fs.DirEntry
Bounds image.Rectangle
Drawer textdraw.Drawer
TextPoint image.Point
}
type historyEntry struct {
location string
filesystem ReadDirStatFS
}
// Directory displays a list of files within a particular directory and
// file system.
type Directory struct {
*core.Core
*core.Propagator
core core.CoreControl
children []fileLayoutEntry
scroll image.Point
contentBounds image.Rectangle
config config.Wrapped
theme theme.Wrapped
onScrollBoundsChange func ()
history []historyEntry
historyIndex int
onChoose func (file string)
}
// NewDirectory creates a new directory view. If within is nil, it will use
// the OS file system.
func NewDirectory (
location string,
within ReadDirStatFS,
) (
element *Directory,
err error,
) {
element = &Directory { }
element.theme.Case = tomo.C("files", "directory")
element.Core, element.core = core.NewCore(element, element.redoAll)
element.Propagator = core.NewPropagator(element, element.core)
err = element.SetLocation(location, within)
return
}
// Location returns the directory's location and filesystem.
func (element *Directory) Location () (string, ReadDirStatFS) {
if len(element.history) < 1 { return "", nil }
current := element.history[element.historyIndex]
return current.location, current.filesystem
}
// SetLocation sets the directory's location and filesystem. If within is nil,
// it will use the OS file system.
func (element *Directory) SetLocation (
location string,
within ReadDirStatFS,
) error {
if within == nil {
within = defaultFS { }
}
element.scroll = image.Point { }
if element.history != nil {
element.historyIndex ++
}
element.history = append (
element.history[:element.historyIndex],
historyEntry { location, within })
return element.Update()
}
// Backward goes back a directory in history
func (element *Directory) Backward () (bool, error) {
if element.historyIndex > 1 {
element.historyIndex --
return true, element.Update()
} else {
return false, nil
}
}
// Forward goes forward a directory in history
func (element *Directory) Forward () (bool, error) {
if element.historyIndex < len(element.history) - 1 {
element.historyIndex ++
return true, element.Update()
} else {
return false, nil
}
}
// Update refreshes the directory's contents.
func (element *Directory) Update () error {
location, filesystem := element.Location()
entries, err := filesystem.ReadDir(location)
// disown all entries
for _, file := range element.children {
file.DrawTo(nil, image.Rectangle { }, nil)
file.SetParent(nil)
if file.Focused() {
file.HandleUnfocus()
}
}
element.children = make([]fileLayoutEntry, len(entries))
for index, entry := range entries {
filePath := filepath.Join(location, entry.Name())
file, _ := NewFile(filePath, filesystem)
file.SetParent(element)
file.OnChoose (func () {
if element.onChoose != nil {
element.onChoose(filePath)
}
})
element.children[index].File = file
element.children[index].DirEntry = entry
element.children[index].Drawer.SetFace (element.theme.FontFace(
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.children[index].Drawer.SetText([]rune(entry.Name()))
element.children[index].Drawer.SetAlign(textdraw.AlignCenter)
}
if element.core.HasImage() {
element.redoAll()
element.core.DamageAll()
}
return err
}
// OnChoose sets a function to be called when the user double-clicks a file or
// sub-directory within the directory view.
func (element *Directory) OnChoose (callback func (file string)) {
element.onChoose = callback
}
// CountChildren returns the amount of children contained within this element.
func (element *Directory) CountChildren () (count int) {
return len(element.children)
}
// Child returns the child at the specified index. If the index is out of
// bounds, this method will return nil.
func (element *Directory) Child (index int) (child tomo.Element) {
if index < 0 || index > len(element.children) { return }
return element.children[index].File
}
func (element *Directory) HandleMouseDown (x, y int, button input.Button) {
if button == input.ButtonLeft {
var file *File
for _, entry := range element.children {
if image.Pt(x, y).In(entry.Bounds) {
file = entry.File
}
}
if file != nil {
file.SetSelected(!file.Selected())
}
}
element.Propagator.HandleMouseDown(x, y, button)
}
func (element *Directory) redoAll () {
if !element.core.HasImage() { return }
// do a layout
element.doLayout()
maxScrollHeight := element.maxScrollHeight()
if element.scroll.Y > maxScrollHeight {
element.scroll.Y = maxScrollHeight
element.doLayout()
}
// draw a background
rocks := make([]image.Rectangle, len(element.children))
for index, entry := range element.children {
rocks[index] = entry.Bounds
}
pattern := element.theme.Pattern (
tomo.PatternPinboard,
tomo.State { })
artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...)
element.partition()
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
parent.NotifyScrollBoundsChange(element)
}
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
// draw labels
foreground := element.theme.Color(tomo.ColorForeground, tomo.State { })
for _, entry := range element.children {
entry.Drawer.Draw(element.core, foreground, entry.TextPoint)
}
}
func (element *Directory) partition () {
for _, entry := range element.children {
entry.DrawTo(nil, entry.Bounds, nil)
}
// cut our canvas up and give peices to child elements
for _, entry := range element.children {
if entry.Bounds.Overlaps(element.Bounds()) {
entry.DrawTo (
canvas.Cut(element.core, entry.Bounds),
entry.Bounds, func (region image.Rectangle) {
element.core.DamageRegion(region)
})
}
}
}
func (element *Directory) Window () tomo.Window {
return element.core.Window()
}
// NotifyMinimumSizeChange notifies the container that the minimum size of a
// child element has changed.
func (element *Directory) NotifyMinimumSizeChange (child tomo.Element) {
element.redoAll()
element.core.DamageAll()
}
// SetTheme sets the element's theme.
func (element *Directory) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.Propagator.SetTheme(new)
element.redoAll()
}
// SetConfig sets the element's configuration.
func (element *Directory) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.Propagator.SetConfig(new)
element.redoAll()
}
// ScrollContentBounds returns the full content size of the element.
func (element *Directory) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *Directory) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternPinboard)
bounds := padding.Apply(element.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *Directory) ScrollTo (position image.Point) {
if position.Y < 0 {
position.Y = 0
}
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scroll = position
if element.core.HasImage() {
element.redoAll()
element.core.DamageAll()
}
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
// bounds, content bounds, or scroll axes change.
func (element *Directory) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// ScrollAxes returns the supported axes for scrolling.
func (element *Directory) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
func (element *Directory) maxScrollHeight () (height int) {
padding := element.theme.Padding(tomo.PatternSunken)
viewportHeight := element.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
func (element *Directory) doLayout () {
margin := element.theme.Margin(tomo.PatternPinboard)
padding := element.theme.Padding(tomo.PatternPinboard)
bounds := padding.Apply(element.Bounds())
element.contentBounds = image.Rectangle { }
beginningOfRow := true
dot := bounds.Min.Sub(element.scroll)
rowHeight := 0
for index, entry := range element.children {
width, height := entry.MinimumSize()
if dot.X + width > bounds.Max.X {
dot.X = bounds.Min.Sub(element.scroll).X
dot.Y += rowHeight
if index > 1 {
dot.Y += margin.Y
}
beginningOfRow = true
}
if beginningOfRow {
beginningOfRow = false
} else {
dot.X += margin.X
}
entry.Drawer.SetMaxWidth(width)
bounds := image.Rect(dot.X, dot.Y, dot.X + width, dot.Y + height)
entry.Bounds = bounds
drawerHeight := entry.Drawer.ReccomendedHeightFor(width)
entry.TextPoint =
image.Pt(bounds.Min.X, bounds.Max.Y + margin.Y).
Sub(entry.Drawer.LayoutBounds().Min)
bounds.Max.Y += margin.Y + drawerHeight
height += margin.Y + drawerHeight
if rowHeight < height {
rowHeight = height
}
element.contentBounds = element.contentBounds.Union(bounds)
element.children[index] = entry
dot.X += width
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
}
func (element *Directory) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternPinboard)
minimumWidth := 0
for _, entry := range element.children {
width, _ := entry.MinimumSize()
if width > minimumWidth {
minimumWidth = width
}
}
element.core.SetMinimumSize (
minimumWidth + padding.Horizontal(),
padding.Vertical())
}

View File

@ -1,4 +1,4 @@
package fileElements
package elements
import "os"
import "io/fs"

View File

@ -5,18 +5,14 @@ import "math"
import "image"
import "image/color"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// AnalogClock can display the time of day in an analog format.
type AnalogClock struct {
*core.Core
core core.CoreControl
time time.Time
config config.Wrapped
entity tomo.Entity
time time.Time
theme theme.Wrapped
}
@ -24,46 +20,24 @@ type AnalogClock struct {
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
element = &AnalogClock { }
element.theme.Case = tomo.C("tomo", "clock")
element.Core, element.core = core.NewCore(element, element.draw)
element.core.SetMinimumSize(64, 64)
element.entity = tomo.NewEntity(element)
element.entity.SetMinimumSize(64, 64)
return
}
// SetTime changes the time that the clock displays.
func (element *AnalogClock) SetTime (newTime time.Time) {
if newTime == element.time { return }
element.time = newTime
element.redo()
// Entity returns this element's entity.
func (element *AnalogClock) Entity () tomo.Entity {
return element.entity
}
// SetTheme sets the element's theme.
func (element *AnalogClock) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.redo()
}
// SetConfig sets the element's configuration.
func (element *AnalogClock) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.redo()
}
func (element *AnalogClock) redo () {
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *AnalogClock) draw () {
bounds := element.Bounds()
// Draw causes the element to draw to the specified destination canvas.
func (element *AnalogClock) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
state := tomo.State { }
pattern := element.theme.Pattern(tomo.PatternSunken, state)
padding := element.theme.Padding(tomo.PatternSunken)
pattern.Draw(element.core, bounds)
pattern.Draw(destination, bounds)
bounds = padding.Apply(bounds)
@ -72,6 +46,7 @@ func (element *AnalogClock) draw () {
for hour := 0; hour < 12; hour ++ {
element.radialLine (
destination,
foreground,
0.8, 0.9, float64(hour) / 6 * math.Pi)
}
@ -80,25 +55,40 @@ func (element *AnalogClock) draw () {
minute := float64(element.time.Minute()) + second / 60
hour := float64(element.time.Hour()) + minute / 60
element.radialLine(foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
element.radialLine(foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
element.radialLine(accent, 0, 0.7, (second - 15) / 30 * math.Pi)
element.radialLine(destination, foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
element.radialLine(destination, foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
element.radialLine(destination, accent, 0, 0.7, (second - 15) / 30 * math.Pi)
}
// SetTime changes the time that the clock displays.
func (element *AnalogClock) SetTime (newTime time.Time) {
if newTime == element.time { return }
element.time = newTime
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *AnalogClock) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.entity.Invalidate()
}
func (element *AnalogClock) radialLine (
destination canvas.Canvas,
source color.RGBA,
inner float64,
outer float64,
radian float64,
) {
bounds := element.Bounds()
bounds := element.entity.Bounds()
width := float64(bounds.Dx()) / 2
height := float64(bounds.Dy()) / 2
min := element.Bounds().Min.Add(image.Pt (
min := bounds.Min.Add(image.Pt (
int(math.Cos(radian) * inner * width + width),
int(math.Sin(radian) * inner * height + height)))
max := element.Bounds().Min.Add(image.Pt (
max := bounds.Min.Add(image.Pt (
int(math.Cos(radian) * outer * width + width),
int(math.Sin(radian) * outer * height + height)))
shapes.ColorLine(element.core, source, 1, min, max)
shapes.ColorLine(destination, source, 1, min, max)
}

View File

@ -3,8 +3,8 @@ package fun
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
@ -18,21 +18,19 @@ type pianoKey struct {
// Piano is an element that can be used to input midi notes.
type Piano struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
low, high music.Octave
entity tomo.FocusableEntity
config config.Wrapped
theme theme.Wrapped
flatTheme theme.Wrapped
sharpTheme theme.Wrapped
low, high music.Octave
flatKeys []pianoKey
sharpKeys []pianoKey
contentBounds image.Rectangle
enabled bool
pressed *pianoKey
keynavPressed map[music.Note] bool
@ -43,11 +41,7 @@ type Piano struct {
// NewPiano returns a new piano element with a lowest and highest octave,
// inclusive. If low is greater than high, they will be swapped.
func NewPiano (low, high music.Octave) (element *Piano) {
if low > high {
temp := low
low = high
high = temp
}
if low > high { low, high = high, low }
element = &Piano {
low: low,
@ -58,16 +52,68 @@ func NewPiano (low, high music.Octave) (element *Piano) {
element.theme.Case = tomo.C("tomo", "piano")
element.flatTheme.Case = tomo.C("tomo", "piano", "flatKey")
element.sharpTheme.Case = tomo.C("tomo", "piano", "sharpKey")
element.Core, element.core = core.NewCore (element, func () {
element.recalculate()
element.draw()
})
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.core, element.redo)
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *Piano) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Piano) Draw (destination canvas.Canvas) {
element.recalculate()
state := tomo.State {
Focused: element.entity.Focused(),
Disabled: !element.Enabled(),
}
for _, key := range element.flatKeys {
_, keynavPressed := element.keynavPressed[key.Note]
element.drawFlat (
destination,
key.Rectangle,
element.pressed != nil &&
(*element.pressed).Note == key.Note || keynavPressed,
state)
}
for _, key := range element.sharpKeys {
_, keynavPressed := element.keynavPressed[key.Note]
element.drawSharp (
destination,
key.Rectangle,
element.pressed != nil &&
(*element.pressed).Note == key.Note || keynavPressed,
state)
}
pattern := element.theme.Pattern(tomo.PatternPinboard, state)
artist.DrawShatter (
destination, pattern, element.entity.Bounds(),
element.contentBounds)
}
// Focus gives this element input focus.
func (element *Piano) Focus () {
element.entity.Focus()
}
// Enabled returns whether this piano can be played or not.
func (element *Piano) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this piano can be played or not.
func (element *Piano) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// OnPress sets a function to be called when a key is pressed.
func (element *Piano) OnPress (callback func (note music.Note)) {
element.onPress = callback
@ -90,7 +136,7 @@ func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
element.onRelease((*element.pressed).Note)
}
element.pressed = nil
element.redo()
element.entity.Invalidate()
}
func (element *Piano) HandleMotion (x, y int) {
@ -126,7 +172,7 @@ func (element *Piano) pressUnderMouseCursor (point image.Point) {
if element.onPress != nil {
element.onPress((*element.pressed).Note)
}
element.redo()
element.entity.Invalidate()
}
}
@ -186,7 +232,7 @@ func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if element.onPress != nil {
element.onPress(note)
}
element.redo()
element.entity.Invalidate()
}
}
@ -199,7 +245,7 @@ func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if element.onRelease != nil {
element.onRelease(note)
}
element.redo()
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
@ -209,8 +255,7 @@ func (element *Piano) SetTheme (new tomo.Theme) {
element.flatTheme.Theme = new
element.sharpTheme.Theme = new
element.updateMinimumSize()
element.recalculate()
element.redo()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
@ -218,13 +263,12 @@ func (element *Piano) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.recalculate()
element.redo()
element.entity.Invalidate()
}
func (element *Piano) updateMinimumSize () {
padding := element.theme.Padding(tomo.PatternPinboard)
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
pianoKeyWidth * 7 * element.countOctaves() +
padding.Horizontal(),
64 + padding.Vertical())
@ -242,19 +286,12 @@ func (element *Piano) countSharps () int {
return element.countOctaves() * 5
}
func (element *Piano) redo () {
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Piano) recalculate () {
element.flatKeys = make([]pianoKey, element.countFlats())
element.sharpKeys = make([]pianoKey, element.countSharps())
padding := element.theme.Padding(tomo.PatternPinboard)
bounds := padding.Apply(element.Bounds())
padding := element.theme.Padding(tomo.PatternPinboard)
bounds := padding.Apply(element.entity.Bounds())
dot := bounds.Min
note := element.low.Note(0)
@ -285,50 +322,24 @@ func (element *Piano) recalculate () {
}
}
func (element *Piano) draw () {
state := tomo.State {
Focused: element.Focused(),
Disabled: !element.Enabled(),
}
for _, key := range element.flatKeys {
_, keynavPressed := element.keynavPressed[key.Note]
element.drawFlat (
key.Rectangle,
element.pressed != nil &&
(*element.pressed).Note == key.Note || keynavPressed,
state)
}
for _, key := range element.sharpKeys {
_, keynavPressed := element.keynavPressed[key.Note]
element.drawSharp (
key.Rectangle,
element.pressed != nil &&
(*element.pressed).Note == key.Note || keynavPressed,
state)
}
pattern := element.theme.Pattern(tomo.PatternPinboard, state)
artist.DrawShatter (
element.core, pattern, element.Bounds(), element.contentBounds)
}
func (element *Piano) drawFlat (
destination canvas.Canvas,
bounds image.Rectangle,
pressed bool,
state tomo.State,
) {
state.Pressed = pressed
pattern := element.flatTheme.Pattern(tomo.PatternButton, state)
pattern.Draw(element.core, bounds)
pattern.Draw(destination, bounds)
}
func (element *Piano) drawSharp (
destination canvas.Canvas,
bounds image.Rectangle,
pressed bool,
state tomo.State,
) {
state.Pressed = pressed
pattern := element.sharpTheme.Pattern(tomo.PatternButton, state)
pattern.Draw(element.core, bounds)
pattern.Draw(destination, bounds)
}

View File

@ -2,47 +2,72 @@ 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/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
// Icon is an element capable of displaying a singular icon.
type Icon struct {
*core.Core
core core.CoreControl
theme theme.Wrapped
id tomo.Icon
size tomo.IconSize
entity tomo.Entity
theme theme.Wrapped
id tomo.Icon
size tomo.IconSize
}
// Icon creates a new icon element.
func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) {
element = &Icon {
id: id,
size: size,
}
element.entity = tomo.NewEntity(element)
element.theme.Case = tomo.C("tomo", "icon")
element.Core, element.core = core.NewCore(element, element.draw)
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *Icon) Entity () tomo.Entity {
return element.entity
}
// SetIcon sets the element's icon.
func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) {
element.id = id
element.size = size
if element.entity == nil { return }
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Icon) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
if element.entity == nil { return }
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
element.entity.Invalidate()
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Icon) Draw (destination canvas.Canvas) {
if element.entity == nil { return }
bounds := element.entity.Bounds()
state := tomo.State { }
element.theme.
Pattern(tomo.PatternBackground, state).
Draw(destination, bounds)
icon := element.icon()
if icon != nil {
iconBounds := icon.Bounds()
offset := image.Pt (
(bounds.Dx() - iconBounds.Dx()) / 2,
(bounds.Dy() - iconBounds.Dy()) / 2)
icon.Draw (
destination,
element.theme.Color(tomo.ColorForeground, state),
bounds.Min.Add(offset))
}
}
@ -53,28 +78,9 @@ func (element *Icon) icon () artist.Icon {
func (element *Icon) updateMinimumSize () {
icon := element.icon()
if icon == nil {
element.core.SetMinimumSize(0, 0)
element.entity.SetMinimumSize(0, 0)
} else {
bounds := icon.Bounds()
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
}
}
func (element *Icon) draw () {
bounds := element.Bounds()
state := tomo.State { }
element.theme.
Pattern(tomo.PatternBackground, state).
Draw(element.core, bounds)
icon := element.icon()
if icon != nil {
iconBounds := icon.Bounds()
offset := image.Pt (
(bounds.Dx() - iconBounds.Dx()) / 2,
(bounds.Dy() - iconBounds.Dy()) / 2)
icon.Draw (
element.core,
element.theme.Color(tomo.ColorForeground, state),
bounds.Min.Add(offset))
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
}
}

View File

@ -1,25 +1,35 @@
package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
// TODO: this element is lame need to make it better
// Image is an element capable of displaying an image.
type Image struct {
*core.Core
core core.CoreControl
entity tomo.Entity
buffer canvas.Canvas
}
// NewImage creates a new image element.
func NewImage (image image.Image) (element *Image) {
element = &Image { buffer: canvas.FromImage(image) }
element.Core, element.core = core.NewCore(element, element.draw)
bounds := image.Bounds()
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
element.entity = tomo.NewEntity(element)
bounds := element.buffer.Bounds()
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
return
}
func (element *Image) draw () {
(patterns.Texture { Canvas: element.buffer }).
Draw(element.core, element.Bounds())
// Entity returns this element's entity.
func (element *Image) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Image) Draw (destination canvas.Canvas) {
if element.entity == nil { return }
(patterns.Texture { Canvas: element.buffer }).
Draw(destination, element.entity.Bounds())
}

View File

@ -2,16 +2,15 @@ package elements
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// Label is a simple text box.
type Label struct {
*core.Core
core core.CoreControl
entity tomo.FlexibleEntity
align textdraw.Align
wrap bool
text string
@ -19,50 +18,34 @@ type Label struct {
forcedColumns int
forcedRows int
minHeight int
config config.Wrapped
theme theme.Wrapped
onFlexibleHeightChange func ()
}
// 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.Core, element.core = core.NewCore(element, element.handleResize)
element.entity = tomo.NewEntity(element).(tomo.FlexibleEntity)
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.SetWrap(wrap)
element.SetText(text)
return
}
func (element *Label) redo () {
face := element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal)
element.drawer.SetFace(face)
element.updateMinimumSize()
bounds := element.Bounds()
if element.wrap {
element.drawer.SetMaxWidth(bounds.Dx())
element.drawer.SetMaxHeight(bounds.Dy())
}
element.draw()
element.core.DamageAll()
// NewLabelWrapped creates a new label with text wrapping on.
func NewLabelWrapped (text string) (element *Label) {
element = NewLabel(text)
element.SetWrap(true)
return
}
func (element *Label) handleResize () {
bounds := element.Bounds()
if element.wrap {
element.drawer.SetMaxWidth(bounds.Dx())
element.drawer.SetMaxHeight(bounds.Dy())
}
element.draw()
return
// Entity returns this element's entity.
func (element *Label) Entity () tomo.Entity {
return element.entity
}
// EmCollapse forces a minimum width and height upon the label. The width is
@ -82,17 +65,10 @@ func (element *Label) FlexibleHeightFor (width int) (height int) {
if element.wrap {
return element.drawer.ReccomendedHeightFor(width)
} else {
_, height = element.MinimumSize()
return
return element.minHeight
}
}
// OnFlexibleHeightChange sets a function to be called when the parameters
// affecting this element's flexible height are changed.
func (element *Label) OnFlexibleHeightChange (callback func ()) {
element.onFlexibleHeightChange = callback
}
// SetText sets the label's text.
func (element *Label) SetText (text string) {
if element.text == text { return }
@ -100,11 +76,7 @@ func (element *Label) SetText (text string) {
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
element.entity.Invalidate()
}
// SetWrap sets wether or not the label's text wraps. If the text is set to
@ -119,25 +91,16 @@ func (element *Label) SetWrap (wrap bool) {
}
element.wrap = wrap
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
element.entity.Invalidate()
}
// SetAlign sets the alignment method of the label.
func (element *Label) SetAlign (align textdraw.Align) {
if align == element.align { return }
element.align = align
element.drawer.SetAlign(align)
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
@ -148,11 +111,7 @@ func (element *Label) SetTheme (new tomo.Theme) {
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
@ -160,11 +119,25 @@ func (element *Label) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.entity.Invalidate()
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Label) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
if element.wrap {
element.drawer.SetMaxWidth(bounds.Dx())
element.drawer.SetMaxHeight(bounds.Dy())
}
element.entity.DrawBackground(destination)
textBounds := element.drawer.LayoutBounds()
foreground := element.theme.Color (
tomo.ColorForeground,
tomo.State { })
element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min))
}
func (element *Label) updateMinimumSize () {
@ -176,9 +149,7 @@ func (element *Label) updateMinimumSize () {
em = element.theme.Padding(tomo.PatternBackground)[0]
}
width, height = em, element.drawer.LineHeight().Round()
if element.onFlexibleHeightChange != nil {
element.onFlexibleHeightChange()
}
element.entity.NotifyFlexibleHeightChange()
} else {
bounds := element.drawer.LayoutBounds()
width, height = bounds.Dx(), bounds.Dy()
@ -196,18 +167,6 @@ func (element *Label) updateMinimumSize () {
Mul(fixed.I(element.forcedRows)).Floor()
}
element.core.SetMinimumSize(width, height)
}
func (element *Label) draw () {
element.core.DrawBackground (
element.theme.Pattern(tomo.PatternBackground, tomo.State { }))
bounds := element.Bounds()
textBounds := element.drawer.LayoutBounds()
foreground := element.theme.Color (
tomo.ColorForeground,
tomo.State { })
element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min))
element.minHeight = height
element.entity.SetMinimumSize(width, height)
}

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

@ -1,107 +1,148 @@
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/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// List is an element that contains several objects that a user can select.
type listEntity interface {
tomo.ContainerEntity
tomo.ScrollableEntity
}
type List struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
pressed bool
container
entity listEntity
scroll image.Point
contentBounds image.Rectangle
columnSizes []int
selected int
contentHeight int
forcedMinimumWidth int
forcedMinimumHeight int
theme theme.Wrapped
selectedEntry int
scroll int
entries []ListEntry
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 }
func NewList (columns int, children ...tomo.Element) (element *List) {
if columns < 1 { columns = 1 }
element = &List { selected: -1 }
element.columnSizes = make([]int, columns)
element.theme.Case = tomo.C("tomo", "list")
element.Core, element.core = core.NewCore(element, element.handleResize)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (element.core, func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.entries = make([]ListEntry, len(entries))
for index, entry := range entries {
element.entries[index] = entry
}
element.updateMinimumSize()
element.entity = tomo.NewEntity(element).(listEntity)
element.container.entity = element.entity
element.minimumSize = element.updateMinimumSize
element.init()
element.Adopt(children...)
return
}
func (element *List) handleResize () {
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
func (element *List) Draw (destination canvas.Canvas) {
rocks := make([]image.Rectangle, element.entity.CountChildren())
for index := 0; index < element.entity.CountChildren(); index ++ {
rocks[index] = element.entity.Child(index).Entity().Bounds()
}
if element.scroll > element.maxScrollHeight() {
element.scroll = element.maxScrollHeight()
pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { })
artist.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...)
}
func (element *List) Layout () {
if element.scroll.Y > element.maxScrollHeight() {
element.scroll.Y = element.maxScrollHeight()
}
element.draw()
element.scrollBoundsChange()
margin := element.theme.Margin(tomo.PatternSunken)
padding := element.theme.Padding(tomo.PatternSunken)
bounds := padding.Apply(element.entity.Bounds())
element.contentBounds = image.Rectangle { }
dot := bounds.Min.Sub(element.scroll)
xStart := dot.X
rowHeight := 0
columnIndex := 0
nextLine := func () {
dot.X = xStart
dot.Y += margin.Y
dot.Y += rowHeight
rowHeight = 0
columnIndex = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
child := element.entity.Child(index)
entry := element.scratch[child]
if columnIndex >= len(element.columnSizes) {
nextLine()
}
width := element.columnSizes[columnIndex]
height := int(entry.minSize)
if len(element.columnSizes) == 1 && width < bounds.Dx() {
width = bounds.Dx()
}
if rowHeight < height {
rowHeight = height
}
childBounds := tomo.Bounds (
dot.X, dot.Y,
width, height)
element.entity.PlaceChild(index, childBounds)
element.contentBounds = element.contentBounds.Union(childBounds)
dot.X += width + margin.X
columnIndex ++
}
element.contentBounds =
element.contentBounds.Sub(element.contentBounds.Min)
element.entity.NotifyScrollBoundsChange()
if element.onScrollBoundsChange != nil {
element.onScrollBoundsChange()
}
}
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)
if element.selected == index { return }
if element.selected >= 0 {
element.entity.SelectChild(element.selected, false)
}
element.selected = index
element.entity.SelectChild(index, true)
}
}
func (element *List) HandleChildMouseUp (int, int, input.Button, tomo.Element) { }
func (element *List) HandleChildFlexibleHeightChange (child tomo.Flexible) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *List) DrawBackground (destination canvas.Canvas) {
element.entity.DrawBackground(destination)
}
// 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
}
func (element *List) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize()
element.redo()
}
// 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()
}
func (element *List) redo () {
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
element.scrollBoundsChange()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// Collapse forces a minimum width and height upon the list. If a zero value is
@ -119,118 +160,39 @@ func (element *List) Collapse (width, height int) {
element.forcedMinimumWidth = width
element.forcedMinimumHeight = height
element.updateMinimumSize()
for index, entry := range element.entries {
element.entries[index] = element.resizeEntryToFit(entry)
}
element.redo()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
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)
func (element *List) ScrollContentBounds () image.Rectangle {
return element.contentBounds
}
// 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())
// ScrollViewportBounds returns the size and position of the element's
// viewport relative to ScrollBounds.
func (element *List) ScrollViewportBounds () image.Rectangle {
padding := element.theme.Padding(tomo.PatternBackground)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
return bounds
}
// 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 position.Y < 0 {
position.Y = 0
}
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
maxScrollHeight := element.maxScrollHeight()
if position.Y > maxScrollHeight {
position.Y = maxScrollHeight
}
element.scrollBoundsChange()
}
// ScrollAxes returns the supported axes for scrolling.
func (element *List) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
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
}
// 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
element.scroll = position
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
// OnScrollBoundsChange sets a function to be called when the element's viewport
@ -239,239 +201,73 @@ 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)
// ScrollAxes returns the supported axes for scrolling.
func (element *List) ScrollAxes () (horizontal, vertical bool) {
return false, true
}
// 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) {
func (element *List) maxScrollHeight () (height int) {
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
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
height = element.contentBounds.Dy() - viewportHeight
if height < 0 { height = 0 }
return
}
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
}
margin := element.theme.Margin(tomo.PatternSunken)
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) draw () {
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)
for index := range element.columnSizes {
element.columnSizes[index] = 0
}
height := 0
rowHeight := 0
columnIndex := 0
nextLine := func () {
height += rowHeight
rowHeight = 0
columnIndex = 0
}
for index := 0; index < element.entity.CountChildren(); index ++ {
if columnIndex >= len(element.columnSizes) {
if index > 0 { height += margin.Y }
nextLine()
}
child := element.entity.Child(index)
entry := element.scratch[child]
entryWidth, entryHeight := element.entity.ChildMinimumSize(index)
entry.minBreadth = float64(entryWidth)
entry.minSize = float64(entryHeight)
element.scratch[child] = entry
if rowHeight < entryHeight {
rowHeight = entryHeight
}
if element.columnSizes[columnIndex] < entryWidth {
element.columnSizes[columnIndex] = entryWidth
}
columnIndex ++
}
nextLine()
width := 0; for index, size := range element.columnSizes {
width += size
if index > 0 { width += margin.X }
}
width += padding.Horizontal()
height += padding.Vertical()
if element.forcedMinimumHeight > 0 {
height = element.forcedMinimumHeight
}
if element.forcedMinimumWidth > 0 {
width = element.forcedMinimumWidth
}
element.entity.SetMinimumSize(width, height)
}

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

@ -2,14 +2,13 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// ProgressBar displays a visual indication of how far along a task is.
type ProgressBar struct {
*core.Core
core core.CoreControl
entity tomo.Entity
progress float64
config config.Wrapped
@ -19,21 +18,43 @@ 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")
element.Core, element.core = core.NewCore(element, element.draw)
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *ProgressBar) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ProgressBar) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { })
padding := element.theme.Padding(tomo.PatternSunken)
pattern.Draw(destination, bounds)
bounds = padding.Apply(bounds)
meterBounds := image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y)
mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { })
mercury.Draw(destination, meterBounds)
}
// 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
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
@ -41,7 +62,7 @@ func (element *ProgressBar) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.updateMinimumSize()
element.redo()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
@ -49,35 +70,13 @@ func (element *ProgressBar) SetConfig (new tomo.Config) {
if new == nil || new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
element.entity.Invalidate()
}
func (element *ProgressBar) updateMinimumSize() {
padding := element.theme.Padding(tomo.PatternSunken)
innerPadding := element.theme.Padding(tomo.PatternMercury)
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
padding.Horizontal() + innerPadding.Horizontal(),
padding.Vertical() + innerPadding.Vertical())
}
func (element *ProgressBar) redo () {
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *ProgressBar) draw () {
bounds := element.Bounds()
pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { })
padding := element.theme.Padding(tomo.PatternSunken)
pattern.Draw(element.core, bounds)
bounds = padding.Apply(bounds)
meterBounds := image.Rect (
bounds.Min.X, bounds.Min.Y,
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
bounds.Max.Y)
mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { })
mercury.Draw(element.core, meterBounds)
}

232
elements/scroll.go Normal file
View File

@ -0,0 +1,232 @@
package elements
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/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
child tomo.Scrollable
horizontal *ScrollBar
vertical *ScrollBar
config config.Wrapped
theme theme.Wrapped
}
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 mode.Includes(ScrollHorizontal) {
element.horizontal = NewScrollBar(false)
element.horizontal.OnScroll (func (viewport image.Point) {
if element.child != nil {
element.child.ScrollTo(viewport)
}
if element.vertical != nil {
element.vertical.SetBounds (
element.child.ScrollContentBounds(),
element.child.ScrollViewportBounds())
}
})
element.entity.Adopt(element.horizontal)
}
if mode.Includes(ScrollVertical) {
element.vertical = NewScrollBar(true)
element.vertical.OnScroll (func (viewport image.Point) {
if element.child != nil {
element.child.ScrollTo(viewport)
}
if element.horizontal != nil {
element.horizontal.SetBounds (
element.child.ScrollContentBounds(),
element.child.ScrollViewportBounds())
}
})
element.entity.Adopt(element.vertical)
}
element.Adopt(child)
return
}
func (element *Scroll) Entity () tomo.Entity {
return element.entity
}
func (element *Scroll) Draw (destination canvas.Canvas) {
if element.horizontal != nil && element.vertical != nil {
bounds := element.entity.Bounds()
bounds.Min = image.Pt (
bounds.Max.X - element.vertical.Entity().Bounds().Dx(),
bounds.Max.Y - element.horizontal.Entity().Bounds().Dy())
state := tomo.State { }
deadArea := element.theme.Pattern(tomo.PatternDead, state)
deadArea.Draw(canvas.Cut(destination, bounds), bounds)
}
}
func (element *Scroll) Layout () {
bounds := element.entity.Bounds()
child := bounds
iHorizontal := element.entity.IndexOf(element.horizontal)
iVertical := element.entity.IndexOf(element.vertical)
iChild := element.entity.IndexOf(element.child)
var horizontal, vertical image.Rectangle
if element.horizontal != nil {
_, hMinHeight := element.entity.ChildMinimumSize(iHorizontal)
child.Max.Y -= hMinHeight
}
if element.vertical != nil {
vMinWidth, _ := element.entity.ChildMinimumSize(iVertical)
child.Max.X -= vMinWidth
}
horizontal.Min.X = bounds.Min.X
horizontal.Max.X = child.Max.X
horizontal.Min.Y = child.Max.Y
horizontal.Max.Y = bounds.Max.Y
vertical.Min.X = child.Max.X
vertical.Max.X = bounds.Max.X
vertical.Min.Y = bounds.Min.Y
vertical.Max.Y = child.Max.Y
if element.horizontal != nil {
element.entity.PlaceChild (iHorizontal, horizontal)
}
if element.vertical != nil {
element.entity.PlaceChild(iVertical, vertical)
}
if element.child != nil {
element.entity.PlaceChild(iChild, child)
}
}
func (element *Scroll) DrawBackground (destination canvas.Canvas) {
element.entity.DrawBackground(destination)
}
func (element *Scroll) Adopt (child tomo.Scrollable) {
if element.child != nil {
element.entity.Disown(element.entity.IndexOf(element.child))
}
if child != nil {
element.entity.Adopt(child)
}
element.child = child
element.updateEnabled()
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) {
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Scroll) HandleChildScrollBoundsChange (tomo.Scrollable) {
element.updateEnabled()
viewportBounds := element.child.ScrollViewportBounds()
contentBounds := element.child.ScrollContentBounds()
if element.horizontal != nil {
element.horizontal.SetBounds(contentBounds, viewportBounds)
}
if element.vertical != nil {
element.vertical.SetBounds(contentBounds, viewportBounds)
}
}
func (element *Scroll) HandleScroll (
x, y int,
deltaX, deltaY float64,
) {
horizontal, vertical := element.child.ScrollAxes()
if !horizontal { deltaX = 0 }
if !vertical { deltaY = 0 }
element.scrollChildBy(int(deltaX), int(deltaY))
}
func (element *Scroll) SetTheme (theme tomo.Theme) {
if theme == element.theme.Theme { return }
element.theme.Theme = theme
element.updateMinimumSize()
element.entity.Invalidate()
element.entity.InvalidateLayout()
}
func (element *Scroll) SetConfig (config tomo.Config) {
element.config.Config = config
}
func (element *Scroll) updateMinimumSize () {
var width, height int
if element.child != nil {
width, height = element.entity.ChildMinimumSize (
element.entity.IndexOf(element.child))
}
if element.horizontal != nil {
hMinWidth, hMinHeight := element.entity.ChildMinimumSize (
element.entity.IndexOf(element.horizontal))
height += hMinHeight
if hMinWidth > width {
width = hMinWidth
}
}
if element.vertical != nil {
vMinWidth, vMinHeight := element.entity.ChildMinimumSize (
element.entity.IndexOf(element.vertical))
width += vMinWidth
if vMinHeight > height {
height = vMinHeight
}
}
element.entity.SetMinimumSize(width, height)
}
func (element *Scroll) updateEnabled () {
horizontal, vertical := false, false
if element.child != nil {
horizontal, vertical = element.child.ScrollAxes()
}
if element.horizontal != nil {
element.horizontal.SetEnabled(horizontal)
}
if element.vertical != nil {
element.vertical.SetEnabled(vertical)
}
}
func (element *Scroll) scrollChildBy (x, y int) {
if element.child == nil { return }
scrollPoint :=
element.child.ScrollViewportBounds().Min.
Add(image.Pt(x, y))
element.child.ScrollTo(scrollPoint)
}

View File

@ -3,10 +3,17 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
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
@ -19,8 +26,7 @@ import "git.tebibyte.media/sashakoshka/tomo/default/config"
// Typically, you wont't want to use a ScrollBar by itself. A ScrollContainer is
// better for most cases.
type ScrollBar struct {
*core.Core
core core.CoreControl
entity tomo.ContainerEntity
vertical bool
enabled bool
@ -38,28 +44,42 @@ 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")
}
element.Core, element.core = core.NewCore(element, element.handleResize)
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
element.updateMinimumSize()
return
}
func (element *ScrollBar) handleResize () {
if element.core.HasImage() {
element.recalculate()
element.draw()
// Entity returns this element's entity.
func (element *ScrollBar) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *ScrollBar) Draw (destination canvas.Canvas) {
element.recalculate()
bounds := element.entity.Bounds()
state := tomo.State {
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
element.theme.Pattern(tomo.PatternGutter, state).Draw (
destination,
bounds)
element.theme.Pattern(tomo.PatternHandle, state).Draw (
destination,
element.bar)
}
func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) {
@ -69,10 +89,10 @@ func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) {
if point.In(element.bar) {
// the mouse is pressed down within the bar's handle
element.dragging = true
element.drawAndPush()
element.entity.Invalidate()
element.dragOffset =
point.Sub(element.bar.Min).
Add(element.Bounds().Min)
Add(element.entity.Bounds().Min)
element.dragTo(point)
} else {
// the mouse is pressed down within the bar's gutter
@ -112,7 +132,7 @@ func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) {
func (element *ScrollBar) HandleMouseUp (x, y int, button input.Button) {
if element.dragging {
element.dragging = false
element.drawAndPush()
element.entity.Invalidate()
}
}
@ -134,7 +154,7 @@ func (element *ScrollBar) HandleScroll (x, y int, deltaX, deltaY float64) {
func (element *ScrollBar) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.drawAndPush()
element.entity.Invalidate()
}
// Enabled returns whether or not the element is enabled.
@ -146,8 +166,7 @@ func (element *ScrollBar) Enabled () (enabled bool) {
func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) {
element.contentBounds = content
element.viewportBounds = viewport
element.recalculate()
element.drawAndPush()
element.entity.Invalidate()
}
// OnScroll sets a function to be called when the user tries to move the scroll
@ -163,7 +182,7 @@ func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) {
func (element *ScrollBar) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawAndPush()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
@ -171,7 +190,7 @@ func (element *ScrollBar) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.drawAndPush()
element.entity.Invalidate()
}
func (element *ScrollBar) isAfterHandle (point image.Point) bool {
@ -184,10 +203,10 @@ func (element *ScrollBar) isAfterHandle (point image.Point) bool {
func (element *ScrollBar) fallbackDragOffset () image.Point {
if element.vertical {
return element.Bounds().Min.
return element.entity.Bounds().Min.
Add(image.Pt(0, element.bar.Dy() / 2))
} else {
return element.Bounds().Min.
return element.entity.Bounds().Min.
Add(image.Pt(element.bar.Dx() / 2, 0))
}
}
@ -236,7 +255,7 @@ func (element *ScrollBar) recalculate () {
}
func (element *ScrollBar) recalculateVertical () {
bounds := element.Bounds()
bounds := element.entity.Bounds()
padding := element.theme.Padding(tomo.PatternGutter)
element.track = padding.Apply(bounds)
@ -263,7 +282,7 @@ func (element *ScrollBar) recalculateVertical () {
}
func (element *ScrollBar) recalculateHorizontal () {
bounds := element.Bounds()
bounds := element.entity.Bounds()
padding := element.theme.Padding(tomo.PatternGutter)
element.track = padding.Apply(bounds)
@ -293,33 +312,12 @@ func (element *ScrollBar) updateMinimumSize () {
gutterPadding := element.theme.Padding(tomo.PatternGutter)
handlePadding := element.theme.Padding(tomo.PatternHandle)
if element.vertical {
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
} else {
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
gutterPadding.Vertical() + handlePadding.Vertical())
}
}
func (element *ScrollBar) drawAndPush () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *ScrollBar) draw () {
bounds := element.Bounds()
state := tomo.State {
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
element.theme.Pattern(tomo.PatternGutter, state).Draw (
element.core,
bounds)
element.theme.Pattern(tomo.PatternHandle, state).Draw (
element.core,
element.bar)
}

View File

@ -3,23 +3,21 @@ package elements
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// Slider is a slider control with a floating point value between zero and one.
type Slider struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
value float64
vertical bool
dragging bool
entity tomo.FocusableEntity
value float64
vertical bool
dragging bool
enabled bool
dragOffset int
track image.Rectangle
bar image.Rectangle
track image.Rectangle
bar image.Rectangle
config config.Wrapped
theme theme.Wrapped
@ -28,25 +26,77 @@ 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")
}
element.Core, element.core = core.NewCore(element, element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.core, element.redo)
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.updateMinimumSize()
return
}
// Entity returns this element's entity.
func (element *Slider) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Slider) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
element.track = element.theme.Padding(tomo.PatternGutter).Apply(bounds)
if element.vertical {
barSize := element.track.Dx()
element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min)
barOffset :=
float64(element.track.Dy() - barSize) *
(1 - element.value)
element.bar = element.bar.Add(image.Pt(0, int(barOffset)))
} else {
barSize := element.track.Dy()
element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min)
barOffset :=
float64(element.track.Dx() - barSize) *
element.value
element.bar = element.bar.Add(image.Pt(int(barOffset), 0))
}
state := tomo.State {
Disabled: !element.Enabled(),
Focused: element.entity.Focused(),
Pressed: element.dragging,
}
element.theme.Pattern(tomo.PatternGutter, state).Draw(destination, bounds)
element.theme.Pattern(tomo.PatternHandle, state).Draw(destination, bounds)
}
// Focus gives this element input focus.
func (element *Slider) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this slider can be dragged or not.
func (element *Slider) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this slider can be dragged or not.
func (element *Slider) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
func (element *Slider) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *Slider) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
@ -56,7 +106,7 @@ func (element *Slider) HandleMouseDown (x, y int, button input.Button) {
if element.onSlide != nil {
element.onSlide()
}
element.redo()
element.entity.Invalidate()
}
}
@ -66,7 +116,7 @@ func (element *Slider) HandleMouseUp (x, y int, button input.Button) {
if element.onRelease != nil {
element.onRelease()
}
element.redo()
element.entity.Invalidate()
}
func (element *Slider) HandleMotion (x, y int) {
@ -76,7 +126,7 @@ func (element *Slider) HandleMotion (x, y int) {
if element.onSlide != nil {
element.onSlide()
}
element.redo()
element.entity.Invalidate()
}
}
@ -110,11 +160,6 @@ func (element *Slider) Value () (value float64) {
return element.value
}
// SetEnabled sets whether or not the slider can be interacted with.
func (element *Slider) SetEnabled (enabled bool) {
element.focusableControl.SetEnabled(enabled)
}
// SetValue sets the slider's value.
func (element *Slider) SetValue (value float64) {
if value < 0 { value = 0 }
@ -126,7 +171,7 @@ func (element *Slider) SetValue (value float64) {
if element.onRelease != nil {
element.onRelease()
}
element.redo()
element.entity.Invalidate()
}
// OnSlide sets a function to be called every time the slider handle changes
@ -144,7 +189,7 @@ func (element *Slider) OnRelease (callback func ()) {
func (element *Slider) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.redo()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
@ -152,7 +197,7 @@ func (element *Slider) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
element.entity.Invalidate()
}
func (element *Slider) changeValue (delta float64) {
@ -166,7 +211,7 @@ func (element *Slider) changeValue (delta float64) {
if element.onRelease != nil {
element.onRelease()
}
element.redo()
element.entity.Invalidate()
}
func (element *Slider) valueFor (x, y int) (value float64) {
@ -190,51 +235,12 @@ func (element *Slider) updateMinimumSize () {
gutterPadding := element.theme.Padding(tomo.PatternGutter)
handlePadding := element.theme.Padding(tomo.PatternHandle)
if element.vertical {
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal(),
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
} else {
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
gutterPadding.Vertical() + handlePadding.Vertical())
}
}
func (element *Slider) redo () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Slider) draw () {
bounds := element.Bounds()
element.track = element.theme.Padding(tomo.PatternGutter).Apply(bounds)
if element.vertical {
barSize := element.track.Dx()
element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min)
barOffset :=
float64(element.track.Dy() - barSize) *
(1 - element.value)
element.bar = element.bar.Add(image.Pt(0, int(barOffset)))
} else {
barSize := element.track.Dy()
element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min)
barOffset :=
float64(element.track.Dx() - barSize) *
element.value
element.bar = element.bar.Add(image.Pt(int(barOffset), 0))
}
state := tomo.State {
Focused: element.Focused(),
Disabled: !element.Enabled(),
Pressed: element.dragging,
}
element.theme.Pattern(tomo.PatternGutter, state).Draw (
element.core,
bounds)
element.theme.Pattern(tomo.PatternHandle, state).Draw (
element.core,
element.bar)
}

View File

@ -1,86 +1,87 @@
package elements
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// Spacer can be used to put space between two elements..
type Spacer struct {
*core.Core
core core.CoreControl
entity tomo.Entity
line bool
config config.Wrapped
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.Core, element.core = core.NewCore(element, element.draw)
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
}
// Draw causes the element to draw to the specified destination canvas.
func (element *Spacer) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
if element.line {
pattern := element.theme.Pattern (
tomo.PatternLine,
tomo.State { })
pattern.Draw(destination, bounds)
} else {
pattern := element.theme.Pattern (
tomo.PatternBackground,
tomo.State { })
pattern.Draw(destination, bounds)
}
}
/// SetLine sets whether or not the spacer will appear as a colored line.
func (element *Spacer) SetLine (line bool) {
if element.line == line { return }
element.line = line
element.updateMinimumSize()
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Spacer) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.redo()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Spacer) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.redo()
element.entity.Invalidate()
}
func (element *Spacer) updateMinimumSize () {
if element.line {
padding := element.theme.Padding(tomo.PatternLine)
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
padding.Horizontal(),
padding.Vertical())
} else {
element.core.SetMinimumSize(1, 1)
}
}
func (element *Spacer) redo () {
if !element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
}
func (element *Spacer) draw () {
bounds := element.Bounds()
if element.line {
pattern := element.theme.Pattern (
tomo.PatternLine,
tomo.State { })
pattern.Draw(element.core, bounds)
} else {
pattern := element.theme.Pattern (
tomo.PatternBackground,
tomo.State { })
pattern.Draw(element.core, bounds)
element.entity.SetMinimumSize(1, 1)
}
}

View File

@ -3,20 +3,18 @@ package elements
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/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// Switch is a toggle-able on/off switch with an optional label. It is
// functionally identical to Checkbox, but plays a different semantic role.
type Switch struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
entity tomo.FocusableEntity
drawer textdraw.Drawer
enabled bool
pressed bool
checked bool
text string
@ -31,12 +29,11 @@ type Switch struct {
func NewSwitch (text string, on bool) (element *Switch) {
element = &Switch {
checked: on,
text: text,
text: text,
enabled: true,
}
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.theme.Case = tomo.C("tomo", "switch")
element.Core, element.core = core.NewCore(element, element.draw)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.core, element.redo)
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
@ -45,129 +42,25 @@ func NewSwitch (text string, on bool) (element *Switch) {
return
}
func (element *Switch) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
element.redo()
// Entity returns this element's entity.
func (element *Switch) Entity () tomo.Entity {
return element.entity
}
func (element *Switch) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false
within := image.Point { x, y }.
In(element.Bounds())
if within {
element.checked = !element.checked
}
if element.core.HasImage() {
element.draw()
element.core.DamageAll()
}
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter {
element.pressed = true
element.redo()
}
}
func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
element.redo()
if element.onToggle != nil {
element.onToggle()
}
}
}
// OnToggle sets the function to be called when the switch is flipped.
func (element *Switch) OnToggle (callback func ()) {
element.onToggle = callback
}
// Value reports whether or not the switch is currently on.
func (element *Switch) Value () (on bool) {
return element.checked
}
// SetEnabled sets whether this switch can be flipped or not.
func (element *Switch) SetEnabled (enabled bool) {
element.focusableControl.SetEnabled(enabled)
}
// SetText sets the checkbox's label text.
func (element *Switch) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.redo()
}
// SetTheme sets the element's theme.
func (element *Switch) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.redo()
}
// SetConfig sets the element's configuration.
func (element *Switch) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
}
func (element *Switch) redo () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
}
func (element *Switch) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
lineHeight := element.drawer.LineHeight().Round()
if element.text == "" {
element.core.SetMinimumSize(lineHeight * 2, lineHeight)
} else {
element.core.SetMinimumSize (
lineHeight * 2 +
element.theme.Margin(tomo.PatternBackground).X +
textBounds.Dx(),
lineHeight)
}
}
func (element *Switch) draw () {
bounds := element.Bounds()
// Draw causes the element to draw to the specified destination canvas.
func (element *Switch) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
state := tomo.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
Focused: element.entity.Focused(),
Pressed: element.pressed,
On: element.checked,
}
element.core.DrawBackground (
element.theme.Pattern(tomo.PatternBackground, state))
element.entity.DrawBackground(destination)
if element.checked {
handleBounds.Min.X += bounds.Dy()
@ -185,11 +78,11 @@ func (element *Switch) draw () {
gutterPattern := element.theme.Pattern (
tomo.PatternGutter, state)
gutterPattern.Draw(element.core, gutterBounds)
gutterPattern.Draw(destination, gutterBounds)
handlePattern := element.theme.Pattern (
tomo.PatternHandle, state)
handlePattern.Draw(element.core, handleBounds)
handlePattern.Draw(destination, handleBounds)
textBounds := element.drawer.LayoutBounds()
offset := bounds.Min.Add(image.Point {
@ -201,5 +94,121 @@ func (element *Switch) draw () {
offset.X -= textBounds.Min.X
foreground := element.theme.Color(tomo.ColorForeground, state)
element.drawer.Draw(element.core, foreground, offset)
element.drawer.Draw(destination, foreground, offset)
}
func (element *Switch) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *Switch) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
element.Focus()
element.pressed = true
element.entity.Invalidate()
}
func (element *Switch) HandleMouseUp (x, y int, button input.Button) {
if button != input.ButtonLeft || !element.pressed { return }
element.pressed = false
within := image.Point { x, y }.
In(element.entity.Bounds())
if within {
element.checked = !element.checked
}
element.entity.Invalidate()
if within && element.onToggle != nil {
element.onToggle()
}
}
func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter {
element.pressed = true
element.entity.Invalidate()
}
}
func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
if key == input.KeyEnter && element.pressed {
element.pressed = false
element.checked = !element.checked
element.entity.Invalidate()
if element.onToggle != nil {
element.onToggle()
}
}
}
// OnToggle sets the function to be called when the switch is flipped.
func (element *Switch) OnToggle (callback func ()) {
element.onToggle = callback
}
// Value reports whether or not the switch is currently on.
func (element *Switch) Value () (on bool) {
return element.checked
}
// Focus gives this element input focus.
func (element *Switch) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this switch is enabled or not.
func (element *Switch) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this switch can be toggled or not.
func (element *Switch) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// SetText sets the checkbox's label text.
func (element *Switch) SetText (text string) {
if element.text == text { return }
element.text = text
element.drawer.SetText([]rune(text))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetTheme sets the element's theme.
func (element *Switch) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
element.theme.Theme = new
element.drawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
tomo.FontSizeNormal))
element.updateMinimumSize()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Switch) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.entity.Invalidate()
}
func (element *Switch) updateMinimumSize () {
textBounds := element.drawer.LayoutBounds()
lineHeight := element.drawer.LineHeight().Round()
if element.text == "" {
element.entity.SetMinimumSize(lineHeight * 2, lineHeight)
} else {
element.entity.SetMinimumSize (
lineHeight * 2 +
element.theme.Margin(tomo.PatternBackground).X +
textBounds.Dx(),
lineHeight)
}
}

View File

@ -4,11 +4,11 @@ import "fmt"
import "time"
import "image"
import "image/color"
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/shatter"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
@ -16,49 +16,52 @@ import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
// Artist is an element that displays shapes and patterns drawn by the artist
// package in order to test it.
type Artist struct {
*core.Core
core core.CoreControl
entity tomo.Entity
}
// NewArtist creates a new artist test element.
func NewArtist () (element *Artist) {
element = &Artist { }
element.Core, element.core = core.NewCore(element, element.draw)
element.core.SetMinimumSize(240, 240)
element.entity = tomo.NewEntity(element)
element.entity.SetMinimumSize(240, 240)
return
}
func (element *Artist) draw () {
bounds := element.Bounds()
patterns.Uhex(0x000000FF).Draw(element.core, bounds)
func (element *Artist) Entity () tomo.Entity {
return element.entity
}
func (element *Artist) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
patterns.Uhex(0x000000FF).Draw(destination, bounds)
drawStart := time.Now()
// 0, 0 - 3, 0
for x := 0; x < 4; x ++ {
element.colorLines(x + 1, element.cellAt(x, 0).Bounds())
element.colorLines(destination, x + 1, element.cellAt(destination, x, 0).Bounds())
}
// 4, 0
c40 := element.cellAt(4, 0)
c40 := element.cellAt(destination, 4, 0)
shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
shapes.ColorLine (
c40, artist.Hex(0xFF0000FF), 1,
c40.Bounds().Min, c40.Bounds().Max)
// 0, 1
c01 := element.cellAt(0, 1)
c01 := element.cellAt(destination, 0, 1)
shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1)
shapes.FillColorEllipse(element.core, artist.Hex(0x00FF00FF), c01.Bounds())
shapes.FillColorEllipse(destination, artist.Hex(0x00FF00FF), c01.Bounds())
// 1, 1 - 3, 1
for x := 1; x < 4; x ++ {
c := element.cellAt(x, 1)
c := element.cellAt(destination, x, 1)
shapes.StrokeColorRectangle (
element.core, artist.Hex(0x888888FF),
destination, artist.Hex(0x888888FF),
c.Bounds(), 1)
shapes.StrokeColorEllipse (
element.core,
destination,
[]color.RGBA {
artist.Hex(0xFF0000FF),
artist.Hex(0x00FF00FF),
@ -68,7 +71,7 @@ func (element *Artist) draw () {
}
// 4, 1
c41 := element.cellAt(4, 1)
c41 := element.cellAt(destination, 4, 1)
shatterPos := c41.Bounds().Min
rocks := []image.Rectangle {
image.Rect(3, 12, 13, 23).Add(shatterPos),
@ -85,46 +88,46 @@ func (element *Artist) draw () {
patterns.Uhex(0xFF00FFFF),
patterns.Uhex(0xFFFF00FF),
patterns.Uhex(0x00FFFFFF),
} [index % 5].Draw(element.core, tile)
} [index % 5].Draw(destination, tile)
}
// 0, 2
c02 := element.cellAt(0, 2)
c02 := element.cellAt(destination, 0, 2)
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
shapes.FillEllipse(c02, c41, c02.Bounds())
// 1, 2
c12 := element.cellAt(1, 2)
c12 := element.cellAt(destination, 1, 2)
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
// 2, 2
c22 := element.cellAt(2, 2)
c22 := element.cellAt(destination, 2, 2)
shapes.FillRectangle(c22, c41, c22.Bounds())
// 3, 2
c32 := element.cellAt(3, 2)
c32 := element.cellAt(destination, 3, 2)
shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5)
// 4, 2
c42 := element.cellAt(4, 2)
c42 := element.cellAt(destination, 4, 2)
// 0, 3
c03 := element.cellAt(0, 3)
c03 := element.cellAt(destination, 0, 3)
patterns.Border {
Canvas: element.thingy(c42),
Inset: artist.Inset { 8, 8, 8, 8 },
}.Draw(c03, c03.Bounds())
// 1, 3
c13 := element.cellAt(1, 3)
c13 := element.cellAt(destination, 1, 3)
patterns.Border {
Canvas: element.thingy(c42),
Inset: artist.Inset { 8, 8, 8, 8 },
}.Draw(c13, c13.Bounds().Inset(10))
// 2, 3
c23 := element.cellAt(2, 3)
c23 := element.cellAt(destination, 2, 3)
patterns.Border {
Canvas: element.thingy(c42),
Inset: artist.Inset { 8, 8, 8, 8 },
@ -143,51 +146,51 @@ func (element *Artist) draw () {
drawTime.Milliseconds(),
drawTime.Microseconds())))
textDrawer.Draw (
element.core, artist.Hex(0xFFFFFFFF),
destination, artist.Hex(0xFFFFFFFF),
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
}
func (element *Artist) colorLines (weight int, bounds image.Rectangle) {
func (element *Artist) colorLines (destination canvas.Canvas, weight int, bounds image.Rectangle) {
bounds = bounds.Inset(4)
c := artist.Hex(0xFFFFFFFF)
shapes.ColorLine(element.core, c, weight, bounds.Min, bounds.Max)
shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max)
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Max.X, bounds.Min.Y),
image.Pt(bounds.Min.X, bounds.Max.Y))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2))
shapes.ColorLine (
element.core, c, weight,
destination, c, weight,
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y),
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
}
func (element *Artist) cellAt (x, y int) (canvas.Canvas) {
bounds := element.Bounds()
func (element *Artist) cellAt (destination canvas.Canvas, x, y int) (canvas.Canvas) {
bounds := element.entity.Bounds()
cellBounds := image.Rectangle { }
cellBounds.Min = bounds.Min
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4
return canvas.Cut (element.core, cellBounds.Add (image.Pt (
return canvas.Cut (destination, cellBounds.Add (image.Pt (
x * cellBounds.Dx(),
y * cellBounds.Dy())))
}

View File

@ -4,17 +4,16 @@ import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/input"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
// Mouse is an element capable of testing mouse input. When the mouse is clicked
// and dragged on it, it draws a trail.
type Mouse struct {
*core.Core
core core.CoreControl
drawing bool
entity tomo.Entity
pressed bool
lastMousePos image.Point
config config.Wrapped
@ -24,69 +23,63 @@ type Mouse struct {
// NewMouse creates a new mouse test element.
func NewMouse () (element *Mouse) {
element = &Mouse { }
element.theme.Case = tomo.C("tomo", "piano")
element.Core, element.core = core.NewCore(element, element.draw)
element.core.SetMinimumSize(32, 32)
element.theme.Case = tomo.C("tomo", "mouse")
element.entity = tomo.NewEntity(element)
element.entity.SetMinimumSize(32, 32)
return
}
func (element *Mouse) Entity () tomo.Entity {
return element.entity
}
func (element *Mouse) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
accent := element.theme.Color (
tomo.ColorAccent,
tomo.State { })
shapes.FillColorRectangle(destination, accent, bounds)
shapes.StrokeColorRectangle (
destination,
artist.Hex(0x000000FF),
bounds, 1)
shapes.ColorLine (
destination, artist.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, 1)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
shapes.ColorLine (
destination, artist.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
if element.pressed {
shapes.ColorLine (
destination, artist.Hex(0x000000FF), 1,
bounds.Min, element.lastMousePos)
}
}
// SetTheme sets the element's theme.
func (element *Mouse) SetTheme (new tomo.Theme) {
element.theme.Theme = new
element.redo()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
func (element *Mouse) SetConfig (new tomo.Config) {
element.config.Config = new
element.redo()
}
func (element *Mouse) redo () {
if !element.core.HasImage() { return }
element.draw()
element.core.DamageAll()
}
func (element *Mouse) draw () {
bounds := element.Bounds()
accent := element.theme.Color (
tomo.ColorAccent,
tomo.State { })
shapes.FillColorRectangle(element.core, accent, bounds)
shapes.StrokeColorRectangle (
element.core,
artist.Hex(0x000000FF),
bounds, 1)
shapes.ColorLine (
element.core, artist.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, 1)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2)))
shapes.ColorLine (
element.core, artist.Hex(0xFFFFFFFF), 1,
bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)),
bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1)))
element.entity.Invalidate()
}
func (element *Mouse) HandleMouseDown (x, y int, button input.Button) {
element.drawing = true
element.lastMousePos = image.Pt(x, y)
element.pressed = true
}
func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
element.drawing = false
mousePos := image.Pt(x, y)
element.core.DamageRegion (shapes.ColorLine (
element.core, artist.Hex(0x000000FF), 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
element.pressed = false
}
func (element *Mouse) HandleMotion (x, y int) {
if !element.drawing { return }
mousePos := image.Pt(x, y)
element.core.DamageRegion (shapes.ColorLine (
element.core, artist.Hex(0x000000FF), 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
if !element.pressed { return }
element.lastMousePos = image.Pt(x, y)
element.entity.Invalidate()
}

View File

@ -12,17 +12,20 @@ import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type textBoxEntity interface {
tomo.FocusableEntity
tomo.ScrollableEntity
tomo.LayoutEntity
}
// TextBox is a single-line text input.
type TextBox struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
entity textBoxEntity
enabled bool
lastClick time.Time
dragging int
dot textmanip.Dot
@ -46,16 +49,9 @@ type TextBox struct {
// a value. When the value is empty, the placeholder will be displayed in gray
// text.
func NewTextBox (placeholder, value string) (element *TextBox) {
element = &TextBox { }
element = &TextBox { enabled: true }
element.theme.Case = tomo.C("tomo", "textBox")
element.Core, element.core = core.NewCore(element, element.handleResize)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore (element.core, func () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
}
})
element.entity = tomo.NewEntity(element).(textBoxEntity)
element.placeholder = placeholder
element.placeholderDrawer.SetFace (element.theme.FontFace (
tomo.FontStyleRegular,
@ -69,17 +65,87 @@ func NewTextBox (placeholder, value string) (element *TextBox) {
return
}
func (element *TextBox) handleResize () {
element.scrollToCursor()
element.draw()
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
parent.NotifyScrollBoundsChange(element)
// Entity returns this element's entity.
func (element *TextBox) Entity () tomo.Entity {
return element.entity
}
// Draw causes the element to draw to the specified destination canvas.
func (element *TextBox) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
state := element.state()
pattern := element.theme.Pattern(tomo.PatternInput, state)
padding := element.theme.Padding(tomo.PatternInput)
innerCanvas := canvas.Cut(destination, padding.Apply(bounds))
pattern.Draw(destination, bounds)
offset := element.textOffset()
if element.entity.Focused() && !element.dot.Empty() {
// draw selection bounds
accent := element.theme.Color(tomo.ColorAccent, state)
canon := element.dot.Canon()
foff := fixedutil.Pt(offset)
start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
end := element.valueDrawer.PositionAt(canon.End).Add(foff)
end.Y += element.valueDrawer.LineHeight()
shapes.FillColorRectangle (
innerCanvas,
accent,
image.Rectangle {
fixedutil.RoundPt(start),
fixedutil.RoundPt(end),
})
}
if len(element.text) == 0 {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
foreground := element.theme.Color (
tomo.ColorForeground,
tomo.State { Disabled: true })
element.placeholderDrawer.Draw (
innerCanvas,
foreground,
offset.Sub(textBounds.Min))
} else {
// draw input value
textBounds := element.valueDrawer.LayoutBounds()
foreground := element.theme.Color(tomo.ColorForeground, state)
element.valueDrawer.Draw (
innerCanvas,
foreground,
offset.Sub(textBounds.Min))
}
if element.entity.Focused() && element.dot.Empty() {
// draw cursor
foreground := element.theme.Color(tomo.ColorForeground, state)
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
shapes.ColorLine (
innerCanvas,
foreground, 1,
cursorPosition.Add(offset),
image.Pt (
cursorPosition.X,
cursorPosition.Y + element.valueDrawer.
LineHeight().Round()).Add(offset))
}
}
// Layout causes the element to perform a layout operation.
func (element *TextBox) Layout () {
element.scrollToCursor()
}
func (element *TextBox) HandleFocusChange () {
element.entity.Invalidate()
}
func (element *TextBox) HandleMouseDown (x, y int, button input.Button) {
if !element.Enabled() { return }
if !element.Focused() { element.Focus() }
element.Focus()
if button == input.ButtonLeft {
runeIndex := element.atPosition(image.Pt(x, y))
@ -94,7 +160,7 @@ func (element *TextBox) HandleMouseDown (x, y int, button input.Button) {
element.lastClick = time.Now()
}
element.redo()
element.entity.Invalidate()
}
}
@ -106,7 +172,7 @@ func (element *TextBox) HandleMotion (x, y int) {
runeIndex := element.atPosition(image.Pt(x, y))
if runeIndex > -1 {
element.dot.End = runeIndex
element.redo()
element.entity.Invalidate()
}
case 2:
@ -125,14 +191,14 @@ func (element *TextBox) HandleMotion (x, y int) {
element.text,
runeIndex)
}
element.redo()
element.entity.Invalidate()
}
}
}
func (element *TextBox) textOffset () image.Point {
padding := element.theme.Padding(tomo.PatternInput)
bounds := element.Bounds()
bounds := element.entity.Bounds()
innerBounds := padding.Apply(bounds)
textHeight := element.valueDrawer.LineHeight().Round()
return bounds.Min.Add (image.Pt (
@ -227,7 +293,7 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers)
element.clipboardPut(element.dot.Slice(element.text))
case key == 'v' && modifiers.Control:
window := element.core.Window()
window := element.entity.Window()
if window == nil { break }
window.Paste (func (d data.Data, err error) {
if err != nil { return }
@ -262,25 +328,17 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers)
}
if (textChanged || scrollMemory != element.scroll) {
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
parent.NotifyScrollBoundsChange(element)
}
element.entity.NotifyScrollBoundsChange()
}
if altered {
element.redo()
}
}
func (element *TextBox) clipboardPut (text []rune) {
window := element.core.Window()
if window != nil {
window.Copy(data.Bytes(data.MimePlain, []byte(string(text))))
element.entity.Invalidate()
}
}
func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
// SetPlaceholder sets the element's placeholder text.
func (element *TextBox) SetPlaceholder (placeholder string) {
if element.placeholder == placeholder { return }
@ -288,9 +346,10 @@ func (element *TextBox) SetPlaceholder (placeholder string) {
element.placeholderDrawer.SetText([]rune(placeholder))
element.updateMinimumSize()
element.redo()
element.entity.Invalidate()
}
// SetValue sets the input's value.
func (element *TextBox) SetValue (text string) {
// if element.text == text { return }
@ -301,27 +360,35 @@ func (element *TextBox) SetValue (text string) {
element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
}
element.scrollToCursor()
element.redo()
element.entity.Invalidate()
}
// Value returns the input's value.
func (element *TextBox) Value () (value string) {
return string(element.text)
}
// Filled returns whether or not this element has a value.
func (element *TextBox) Filled () (filled bool) {
return len(element.text) > 0
}
// OnKeyDown specifies a function to be called when a key is pressed within the
// text input.
func (element *TextBox) OnKeyDown (
callback func (key input.Key, modifiers input.Modifiers) (handled bool),
) {
element.onKeyDown = callback
}
// OnEnter specifies a function to be called when the enter key is pressed
// within this input.
func (element *TextBox) OnEnter (callback func ()) {
element.onEnter = callback
}
// OnChange specifies a function to be called when the value of this input
// changes.
func (element *TextBox) OnChange (callback func ()) {
element.onChange = callback
}
@ -332,6 +399,23 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) {
element.onScrollBoundsChange = callback
}
// Focus gives this element input focus.
func (element *TextBox) Focus () {
if !element.entity.Focused() { element.entity.Focus() }
}
// Enabled returns whether this label can be edited or not.
func (element *TextBox) Enabled () bool {
return element.enabled
}
// SetEnabled sets whether this label can be edited or not.
func (element *TextBox) SetEnabled (enabled bool) {
if element.enabled == enabled { return }
element.enabled = enabled
element.entity.Invalidate()
}
// ScrollContentBounds returns the full content size of the element.
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
bounds = element.valueDrawer.LayoutBounds()
@ -348,11 +432,6 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
0)
}
func (element *TextBox) scrollViewportWidth () (width int) {
padding := element.theme.Padding(tomo.PatternInput)
return padding.Apply(element.Bounds()).Dx()
}
// ScrollTo scrolls the viewport to the specified point relative to
// ScrollBounds.
func (element *TextBox) ScrollTo (position image.Point) {
@ -365,10 +444,8 @@ func (element *TextBox) ScrollTo (position image.Point) {
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
if element.scroll > maxPosition { element.scroll = maxPosition }
element.redo()
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
parent.NotifyScrollBoundsChange(element)
}
element.entity.Invalidate()
element.entity.NotifyScrollBoundsChange()
}
// ScrollAxes returns the supported axes for scrolling.
@ -376,32 +453,6 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
return true, false
}
func (element *TextBox) runOnChange () {
if element.onChange != nil {
element.onChange()
}
}
func (element *TextBox) scrollToCursor () {
if !element.core.HasImage() { return }
padding := element.theme.Padding(tomo.PatternInput)
bounds := padding.Apply(element.Bounds())
bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round()
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
cursorPosition.X -= element.scroll
maxX := bounds.Max.X
minX := maxX
if cursorPosition.X > maxX {
element.scroll += cursorPosition.X - maxX
} else if cursorPosition.X < minX {
element.scroll -= minX - cursorPosition.X
if element.scroll < 0 { element.scroll = 0 }
}
}
// SetTheme sets the element's theme.
func (element *TextBox) SetTheme (new tomo.Theme) {
if new == element.theme.Theme { return }
@ -412,7 +463,7 @@ func (element *TextBox) SetTheme (new tomo.Theme) {
element.placeholderDrawer.SetFace(face)
element.valueDrawer.SetFace(face)
element.updateMinimumSize()
element.redo()
element.entity.Invalidate()
}
// SetConfig sets the element's configuration.
@ -420,13 +471,46 @@ func (element *TextBox) SetConfig (new tomo.Config) {
if new == element.config.Config { return }
element.config.Config = new
element.updateMinimumSize()
element.redo()
element.entity.Invalidate()
}
func (element *TextBox) runOnChange () {
if element.onChange != nil {
element.onChange()
}
}
func (element *TextBox) scrollViewportWidth () (width int) {
padding := element.theme.Padding(tomo.PatternInput)
return padding.Apply(element.entity.Bounds()).Dx()
}
func (element *TextBox) scrollToCursor () {
padding := element.theme.Padding(tomo.PatternInput)
bounds := padding.Apply(element.entity.Bounds())
bounds = bounds.Sub(bounds.Min)
bounds.Max.X -= element.valueDrawer.Em().Round()
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
cursorPosition.X -= element.scroll
maxX := bounds.Max.X
minX := maxX
if cursorPosition.X > maxX {
element.scroll += cursorPosition.X - maxX
element.entity.NotifyScrollBoundsChange()
element.entity.Invalidate()
} else if cursorPosition.X < minX {
element.scroll -= minX - cursorPosition.X
if element.scroll < 0 { element.scroll = 0 }
element.entity.NotifyScrollBoundsChange()
element.entity.Invalidate()
}
}
func (element *TextBox) updateMinimumSize () {
textBounds := element.placeholderDrawer.LayoutBounds()
padding := element.theme.Padding(tomo.PatternInput)
element.core.SetMinimumSize (
element.entity.SetMinimumSize (
padding.Horizontal() + textBounds.Dx(),
padding.Vertical() +
element.placeholderDrawer.LineHeight().Round())
@ -436,81 +520,19 @@ func (element *TextBox) notifyAsyncTextChange () {
element.runOnChange()
element.valueDrawer.SetText(element.text)
element.scrollToCursor()
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
parent.NotifyScrollBoundsChange(element)
}
element.redo()
element.entity.Invalidate()
}
func (element *TextBox) redo () {
if element.core.HasImage () {
element.draw()
element.core.DamageAll()
func (element *TextBox) clipboardPut (text []rune) {
window := element.entity.Window()
if window != nil {
window.Copy(data.Bytes(data.MimePlain, []byte(string(text))))
}
}
func (element *TextBox) draw () {
bounds := element.Bounds()
state := tomo.State {
func (element *TextBox) state () tomo.State {
return tomo.State {
Disabled: !element.Enabled(),
Focused: element.Focused(),
}
pattern := element.theme.Pattern(tomo.PatternInput, state)
padding := element.theme.Padding(tomo.PatternInput)
innerCanvas := canvas.Cut(element.core, padding.Apply(bounds))
pattern.Draw(element.core, bounds)
offset := element.textOffset()
if element.Focused() && !element.dot.Empty() {
// draw selection bounds
accent := element.theme.Color(tomo.ColorAccent, state)
canon := element.dot.Canon()
foff := fixedutil.Pt(offset)
start := element.valueDrawer.PositionAt(canon.Start).Add(foff)
end := element.valueDrawer.PositionAt(canon.End).Add(foff)
end.Y += element.valueDrawer.LineHeight()
shapes.FillColorRectangle (
innerCanvas,
accent,
image.Rectangle {
fixedutil.RoundPt(start),
fixedutil.RoundPt(end),
})
}
if len(element.text) == 0 {
// draw placeholder
textBounds := element.placeholderDrawer.LayoutBounds()
foreground := element.theme.Color (
tomo.ColorForeground,
tomo.State { Disabled: true })
element.placeholderDrawer.Draw (
innerCanvas,
foreground,
offset.Sub(textBounds.Min))
} else {
// draw input value
textBounds := element.valueDrawer.LayoutBounds()
foreground := element.theme.Color(tomo.ColorForeground, state)
element.valueDrawer.Draw (
innerCanvas,
foreground,
offset.Sub(textBounds.Min))
}
if element.Focused() && element.dot.Empty() {
// draw cursor
foreground := element.theme.Color(tomo.ColorForeground, state)
cursorPosition := fixedutil.RoundPt (
element.valueDrawer.PositionAt(element.dot.End))
shapes.ColorLine (
innerCanvas,
foreground, 1,
cursorPosition.Add(offset),
image.Pt (
cursorPosition.X,
cursorPosition.Y + element.valueDrawer.
LineHeight().Round()).Add(offset))
Focused: element.entity.Focused(),
}
}

129
entity.go Normal file
View File

@ -0,0 +1,129 @@
package tomo
import "image"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
// Entity is a handle given to elements by the backend. Different types of
// entities may be assigned to elements that support different capabilities.
type Entity interface {
// Invalidate marks the element's current visual as invalid. At the end
// of every event, the backend will ask all invalid entities to redraw
// themselves.
Invalidate ()
// Bounds returns the bounds of the element to be used for drawing and
// layout.
Bounds () image.Rectangle
// Window returns the window that the element is in.
Window () Window
// SetMinimumSize reports to the system what the element's minimum size
// can be. The minimum size of child elements should be taken into
// account when calculating this.
SetMinimumSize (width, height int)
// DrawBackground asks the parent element to draw its background pattern
// to a canvas. This should be used for transparent elements like text
// labels. If there is no parent element (that is, the element is
// directly inside of the window), the backend will draw a default
// background pattern.
DrawBackground (canvas.Canvas)
}
// LayoutEntity is given to elements that support the Layoutable interface.
type LayoutEntity interface {
Entity
// InvalidateLayout marks the element's layout as invalid. At the end of
// every event, the backend will ask all invalid elements to recalculate
// their layouts.
InvalidateLayout ()
}
// ContainerEntity is given to elements that support the Container interface.
type ContainerEntity interface {
Entity
LayoutEntity
// Adopt adds an element as a child.
Adopt (child Element)
// Insert inserts an element in the child list at the specified
// location.
Insert (index int, child Element)
// Disown removes the child at the specified index.
Disown (index int)
// IndexOf returns the index of the specified child.
IndexOf (child Element) int
// Child returns the child at the specified index.
Child (index int) Element
// CountChildren returns the amount of children the element has.
CountChildren () int
// PlaceChild sets the size and position of the child at the specified
// index to a bounding rectangle.
PlaceChild (index int, bounds image.Rectangle)
// SelectChild marks a child as selected or unselected, if it is
// selectable.
SelectChild (index int, selected bool)
// ChildMinimumSize returns the minimum size of the child at the
// specified index.
ChildMinimumSize (index int) (width, height int)
}
// FocusableEntity is given to elements that support the Focusable interface.
type FocusableEntity interface {
Entity
// Focused returns whether the element currently has input focus.
Focused () bool
// Focus sets this element as focused. If this succeeds, the element will
// recieve a HandleFocus call.
Focus ()
// FocusNext causes the focus to move to the next element. If this
// succeeds, the element will recieve a HandleUnfocus call.
FocusNext ()
// FocusPrevious causes the focus to move to the next element. If this
// succeeds, the element will recieve a HandleUnfocus call.
FocusPrevious ()
}
// SelectableEntity is given to elements that support the Selectable interface.
type SelectableEntity interface {
Entity
// Selected returns whether this element is currently selected.
Selected () bool
}
// FlexibleEntity is given to elements that support the Flexible interface.
type FlexibleEntity interface {
Entity
// NotifyFlexibleHeightChange notifies the system that the parameters
// affecting the element's flexible height have changed. This method is
// expected to be called by flexible elements when their content changes.
NotifyFlexibleHeightChange ()
}
// ScrollableEntity is given to elements that support the Scrollable interface.
type ScrollableEntity interface {
Entity
// NotifyScrollBoundsChange notifies the system that the element's
// scroll content bounds or viewport bounds have changed. This is
// expected to be called by scrollable elements when they change their
// supported scroll axes, their scroll position (either autonomously or
// as a result of a call to ScrollTo()), or their content size.
NotifyScrollBoundsChange ()
}

View File

@ -4,35 +4,27 @@ import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 256))
window.SetTitle("Text alignment")
container := containers.NewDocumentContainer()
scrollContainer := containers.NewScrollContainer(false, true)
scrollContainer.Adopt(container)
window.Adopt(scrollContainer)
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 (elements.ScrollVertical,
elements.NewDocument(left, center, right, justify)))
window.OnClose(tomo.Stop)
window.Show()

View File

@ -2,9 +2,7 @@ package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
@ -15,23 +13,15 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Checkboxes")
container := containers.NewContainer(layouts.Vertical { 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() {
@ -42,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

@ -8,10 +8,8 @@ import _ "image/jpeg"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/data"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -27,9 +25,9 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0))
window.SetTitle("Clipboard")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
textInput := elements.NewTextBox("", "")
controlRow := containers.NewContainer(layouts.Horizontal { true, false })
controlRow := elements.NewHBox(elements.SpaceMargin)
copyButton := elements.NewButton("Copy")
copyButton.SetIcon(tomo.IconCopy)
pasteButton := elements.NewButton("Paste")
@ -109,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)
@ -123,13 +121,15 @@ 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.NewContainer(layouts.Vertical { 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)
closeButton.Focus()
window.Show()
}

View File

@ -1,30 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("dialog")
container := containers.NewContainer(layouts.Dialog { true, true })
window.Adopt(container)
container.Adopt(elements.NewLabel("you will explode", false), true)
cancel := elements.NewButton("Cancel")
cancel.SetEnabled(false)
container.Adopt(cancel, false)
okButton := elements.NewButton("OK")
container.Adopt(okButton, false)
okButton.Focus()
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -6,7 +6,6 @@ import _ "image/png"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -22,47 +21,41 @@ func run () {
file.Close()
if err != nil { panic(err.Error()); return }
scrollContainer := containers.NewScrollContainer(false, true)
document := containers.NewDocumentContainer()
document.Adopt (elements.NewLabel (
"A document container is a vertically stacked container " +
"capable of properly laying out flexible elements such as " +
"text-wrapped labels. You can also include normal elements " +
"like:", true), true)
document.Adopt (elements.NewButton (
"Buttons,"), true)
document.Adopt (elements.NewCheckbox (
"Checkboxes,", true), true)
document.Adopt(elements.NewTextBox("", "And text boxes."), true)
document.Adopt (elements.NewSpacer(true), true)
document.Adopt (elements.NewLabel (
"Document containers are meant to be placed inside of a " +
"ScrollContainer, like this one.", true), true)
document.Adopt (elements.NewLabel (
"You could use document containers to do things like display various " +
"forms of hypertext (like HTML, gemtext, markdown, etc.), " +
"lay out a settings menu with descriptive label text between " +
"control groups like in iOS, or list comment or chat histories.",
true), true)
document.Adopt(elements.NewImage(logo), true)
document.Adopt (elements.NewLabel (
"You can also choose whether each element is on its own line " +
"(sort of like an HTML/CSS block element) or on a line with " +
"other adjacent elements (like an HTML/CSS inline element).",
true), true)
document.Adopt(elements.NewButton("Just"), false)
document.Adopt(elements.NewButton("like"), false)
document.Adopt(elements.NewButton("this."), false)
document.Adopt (elements.NewLabel (
"Oh, you're a switch? Then name all of these switches:",
true), true)
document := elements.NewDocument()
document.Adopt (
elements.NewLabelWrapped (
"A document container is a vertically stacked container " +
"capable of properly laying out flexible elements such as " +
"text-wrapped labels. You can also include normal elements " +
"like:"),
elements.NewButton("Buttons,"),
elements.NewCheckbox("Checkboxes,", true),
elements.NewTextBox("", "And text boxes."),
elements.NewLine(),
elements.NewLabelWrapped (
"Document containers are meant to be placed inside of a " +
"ScrollContainer, like this one."),
elements.NewLabelWrapped (
"You could use document containers to do things like display various " +
"forms of hypertext (like HTML, gemtext, markdown, etc.), " +
"lay out a settings menu with descriptive label text between " +
"control groups like in iOS, or list comment or chat histories."),
elements.NewImage(logo),
elements.NewLabelWrapped (
"You can also choose whether each element is on its own line " +
"(sort of like an HTML/CSS block element) or on a line with " +
"other adjacent elements (like an HTML/CSS inline element)."))
document.AdoptInline (
elements.NewButton("Just"),
elements.NewButton("like"),
elements.NewButton("this."))
document.Adopt (elements.NewLabelWrapped (
"Oh, you're a switch? Then name all of these switches:"))
for i := 0; i < 30; i ++ {
document.Adopt(elements.NewSwitch("", false), false)
document.AdoptInline(elements.NewSwitch("", false))
}
scrollContainer.Adopt(document)
window.Adopt(scrollContainer)
window.Adopt(elements.NewScroll(elements.ScrollVertical, document))
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -3,10 +3,7 @@ package main
import "os"
import "path/filepath"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/file"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
@ -16,11 +13,11 @@ func main () {
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 384, 384))
window.SetTitle("File browser")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
homeDir, _ := os.UserHomeDir()
controlBar := containers.NewContainer(layouts.Horizontal { })
controlBar := elements.NewHBox(elements.SpaceNone)
backButton := elements.NewButton("Back")
backButton.SetIcon(tomo.IconBackward)
backButton.ShowText(false)
@ -35,12 +32,11 @@ func run () {
upwardButton.ShowText(false)
locationInput := elements.NewTextBox("Location", "")
statusBar := containers.NewContainer(layouts.Horizontal { true, false })
directory, _ := fileElements.NewFile(homeDir, nil)
baseName := elements.NewLabel(filepath.Base(homeDir), false)
statusBar := elements.NewHBox(elements.SpaceMargin)
directory, _ := elements.NewFile(homeDir, nil)
baseName := elements.NewLabel(filepath.Base(homeDir))
scrollContainer := containers.NewScrollContainer(false, true)
directoryView, _ := fileElements.NewDirectory(homeDir, nil)
directoryView, _ := elements.NewDirectory(homeDir, nil)
updateStatus := func () {
filePath, _ := directoryView.Location()
directory.SetLocation(filePath, nil)
@ -72,19 +68,15 @@ func run () {
filePath, _ := directoryView.Location()
choose(filepath.Dir(filePath))
})
controlBar.Adopt(backButton, forwardButton, refreshButton, upwardButton)
controlBar.AdoptExpand(locationInput)
statusBar.Adopt(directory, baseName)
controlBar.Adopt(backButton, false)
controlBar.Adopt(forwardButton, false)
controlBar.Adopt(refreshButton, false)
controlBar.Adopt(upwardButton, false)
controlBar.Adopt(locationInput, true)
scrollContainer.Adopt(directoryView)
statusBar.Adopt(directory, false)
statusBar.Adopt(baseName, false)
container.Adopt(controlBar, false)
container.Adopt(scrollContainer, true)
container.Adopt(statusBar, false)
container.Adopt(controlBar)
container.AdoptExpand (
elements.NewScroll(elements.ScrollVertical, directoryView))
container.Adopt(statusBar)
window.OnClose(tomo.Stop)
window.Show()

View File

@ -2,10 +2,8 @@ package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/flow"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -14,15 +12,15 @@ func main () {
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192))
window.SetTitle("adventure")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
var world flow.Flow
world.Transition = container.DisownAll
world.Stages = map [string] func () {
"start": func () {
label := elements.NewLabel (
"you are standing next to a river.", true)
label := elements.NewLabelWrapped (
"you are standing next to a river.")
button0 := elements.NewButton("go in the river")
button0.OnClick(world.SwitchFunc("wet"))
@ -31,81 +29,66 @@ func run () {
button2 := elements.NewButton("turn around")
button2.OnClick(world.SwitchFunc("bear"))
container.Warp ( func () {
container.Adopt(label, true)
container.Adopt(button0, false)
container.Adopt(button1, false)
container.Adopt(button2, false)
button0.Focus()
})
container.AdoptExpand(label)
container.Adopt(button0, button1, button2)
button0.Focus()
},
"wet": func () {
label := elements.NewLabel (
label := elements.NewLabelWrapped (
"you get completely soaked.\n" +
"you die of hypothermia.", true)
"you die of hypothermia.")
button0 := elements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := elements.NewButton("exit")
button1.OnClick(tomo.Stop)
container.Warp (func () {
container.Adopt(label, true)
container.Adopt(button0, false)
container.Adopt(button1, false)
button0.Focus()
})
container.AdoptExpand(label)
container.Adopt(button0, button1)
button0.Focus()
},
"house": func () {
label := elements.NewLabel (
label := elements.NewLabelWrapped (
"you are standing in front of a delapidated " +
"house.", true)
"house.")
button1 := elements.NewButton("go inside")
button1.OnClick(world.SwitchFunc("inside"))
button0 := elements.NewButton("turn back")
button0.OnClick(world.SwitchFunc("start"))
container.Warp (func () {
container.Adopt(label, true)
container.Adopt(button1, false)
container.Adopt(button0, false)
button1.Focus()
})
container.AdoptExpand(label)
container.Adopt(button0, button1)
button1.Focus()
},
"inside": func () {
label := elements.NewLabel (
label := elements.NewLabelWrapped (
"you are standing inside of the house.\n" +
"it is dark, but rays of light stream " +
"through the window.\n" +
"there is nothing particularly interesting " +
"here.", true)
"here.")
button0 := elements.NewButton("go back outside")
button0.OnClick(world.SwitchFunc("house"))
container.Warp (func () {
container.Adopt(label, true)
container.Adopt(button0, false)
button0.Focus()
})
container.AdoptExpand(label)
container.Adopt(button0)
button0.Focus()
},
"bear": func () {
label := elements.NewLabel (
label := elements.NewLabelWrapped (
"you come face to face with a bear.\n" +
"it eats you (it was hungry).", true)
"it eats you (it was hungry).")
button0 := elements.NewButton("try again")
button0.OnClick(world.SwitchFunc("start"))
button1 := elements.NewButton("exit")
button1.OnClick(tomo.Stop)
container.Warp (func () {
container.Adopt(label, true)
container.Adopt(button0, false)
container.Adopt(button1, false)
button0.Focus()
})
container.AdoptExpand(label)
container.Adopt(button0, button1)
button0.Focus()
},
}
world.Switch("start")

View File

@ -3,11 +3,9 @@ package main
import "os"
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -17,13 +15,14 @@ func main () {
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216))
window.SetTitle("Clock")
container := containers.NewContainer(layouts.Vertical { true, true })
window.SetApplicationName("TomoClock")
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
clock := fun.NewAnalogClock(time.Now())
container.Adopt(clock, true)
label := elements.NewLabel(formatTime(), false)
container.Adopt(label, false)
label := elements.NewLabel(formatTime())
container.AdoptExpand(clock)
container.Adopt(label)
window.OnClose(tomo.Stop)
window.Show()

24
examples/hbox/main.go Normal file
View File

@ -0,0 +1,24 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
window.SetTitle("horizontal stack")
container := elements.NewHBox(elements.SpaceBoth)
window.Adopt(container)
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,26 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
window.SetTitle("horizontal stack")
container := containers.NewContainer(layouts.Horizontal { true, true })
window.Adopt(container)
container.Adopt(elements.NewLabel("this is sample text", true), true)
container.Adopt(elements.NewLabel("this is sample text", true), true)
container.Adopt(elements.NewLabel("this is sample text", true), true)
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,10 +1,8 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -14,30 +12,31 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
window.SetTitle("Icons")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
container.Adopt(elements.NewLabel("Just some of the wonderful icons we have:", false), false)
container.Adopt(elements.NewSpacer(true), false)
container.Adopt(icons(tomo.IconHome, tomo.IconHistory), true)
container.Adopt(icons(tomo.IconFile, tomo.IconNetwork), true)
container.Adopt(icons(tomo.IconOpen, tomo.IconRemoveFavorite), true)
container.Adopt(icons(tomo.IconCursor, tomo.IconDistort), true)
container.Adopt (
elements.NewLabel("Just some of the wonderful icons we have:"),
elements.NewLine())
container.AdoptExpand (
icons(tomo.IconHome, tomo.IconHistory),
icons(tomo.IconFile, tomo.IconNetwork),
icons(tomo.IconOpen, tomo.IconRemoveFavorite),
icons(tomo.IconCursor, tomo.IconDistort))
closeButton := elements.NewButton("Ok")
closeButton := elements.NewButton("Yes verynice")
closeButton.SetIcon(tomo.IconYes)
closeButton.ShowText(false)
closeButton.OnClick(tomo.Stop)
container.Adopt(closeButton, false)
container.Adopt(closeButton)
window.OnClose(tomo.Stop)
window.Show()
}
func icons (min, max tomo.Icon) (container *containers.Container) {
container = containers.NewContainer(layouts.Horizontal { true, false })
func icons (min, max tomo.Icon) (container *elements.Box) {
container = elements.NewHBox(elements.SpaceMargin)
for index := min; index <= max; index ++ {
container.Adopt(elements.NewIcon(index, tomo.IconSizeSmall), true)
container.AdoptExpand(elements.NewIcon(index, tomo.IconSizeSmall))
}
return
}

View File

@ -7,17 +7,15 @@ import _ "image/png"
import "github.com/jezek/xgbutil/gopher"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(2, 2)
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Tomo Logo")
file, err := os.Open("assets/banner.png")
@ -26,19 +24,20 @@ func run () {
file.Close()
if err != nil { fatalError(window, err); return }
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
logoImage := elements.NewImage(logo)
button := elements.NewButton("Show me a gopher instead")
button.OnClick (func () { container.Warp (func () {
container.DisownAll()
gopher, _, err :=
image.Decode(bytes.NewReader(gopher.GopherPng()))
if err != nil { fatalError(window, err); return }
container.Adopt(elements.NewImage(gopher),true)
}) })
button.OnClick (func () {
window.SetTitle("Not the Tomo Logo")
container.DisownAll()
gopher, _, err :=
image.Decode(bytes.NewReader(gopher.GopherPng()))
if err != nil { fatalError(window, err); return }
container.AdoptExpand(elements.NewImage(gopher))
})
container.Adopt(logoImage, true)
container.Adopt(button, false)
container.AdoptExpand(logoImage)
container.Adopt(button)
window.Adopt(container)
button.Focus()

View File

@ -2,10 +2,8 @@ package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -14,7 +12,7 @@ func main () {
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Enter Details")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
// create inputs
@ -47,13 +45,8 @@ func run () {
fingerLength.OnChange(check)
// add elements to container
container.Adopt(elements.NewLabel("Choose your words carefully.", false), true)
container.Adopt(firstName, false)
container.Adopt(lastName, false)
container.Adopt(fingerLength, false)
container.Adopt(elements.NewSpacer(true), false)
container.Adopt(button, false)
container.AdoptExpand(elements.NewLabel("Choose your words carefully."))
container.Adopt(firstName, lastName, fingerLength, elements.NewLine(), button)
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -11,7 +11,7 @@ func main () {
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360))
window.SetTitle("example label")
window.Adopt(elements.NewLabel(text, true))
window.Adopt(elements.NewLabelWrapped(text))
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -2,10 +2,8 @@ package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
@ -16,47 +14,54 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0))
window.SetTitle("List Sidebar")
container := containers.NewContainer(layouts.Horizontal { true, true })
container := elements.NewHBox(elements.SpaceBoth)
window.Adopt(container)
var currentPage tomo.Element
turnPage := func (newPage tomo.Element) {
container.Warp (func () {
if currentPage != nil {
container.Disown(currentPage)
}
container.Adopt(newPage, true)
currentPage = newPage
})
if currentPage != nil {
container.Disown(currentPage)
}
container.AdoptExpand(newPage)
currentPage = newPage
}
intro := elements.NewLabel (
intro := elements.NewLabelWrapped (
"The List element can be easily used as a sidebar. " +
"Click on entries to flip pages!", true)
"Click on entries to flip pages!")
button := elements.NewButton("I do nothing!")
button.OnClick (func () {
popups.NewDialog(popups.DialogKindInfo, window, "", "Sike!")
})
mouse := testing.NewMouse()
input := elements.NewTextBox("Write some text", "")
form := containers.NewContainer(layouts.Vertical { true, false})
form.Adopt(elements.NewLabel("I have:", false), false)
form.Adopt(elements.NewSpacer(true), false)
form.Adopt(elements.NewCheckbox("Skin", true), false)
form.Adopt(elements.NewCheckbox("Blood", false), false)
form.Adopt(elements.NewCheckbox("Bone", false), false)
form := elements.NewVBox (
elements.SpaceMargin,
elements.NewLabel("I have:"),
elements.NewLine(),
elements.NewCheckbox("Skin", true),
elements.NewCheckbox("Blood", false),
elements.NewCheckbox("Bone", false))
art := testing.NewArtist()
makePage := func (name string, callback func ()) tomo.Selectable {
cell := elements.NewCell(elements.NewLabel(name))
cell.OnSelectionChange (func () {
if cell.Selected() { callback() }
})
return cell
}
list := elements.NewList (
elements.NewListEntry("button", func () { turnPage(button) }),
elements.NewListEntry("mouse", func () { turnPage(mouse) }),
elements.NewListEntry("input", func () { turnPage(input) }),
elements.NewListEntry("form", func () { turnPage(form) }),
elements.NewListEntry("art", func () { turnPage(art) }))
list.OnNoEntrySelected(func () { turnPage (intro) })
1,
makePage("button", func () { turnPage(button) }),
makePage("mouse", func () { turnPage(mouse) }),
makePage("input", func () { turnPage(input) }),
makePage("form", func () { turnPage(form) }),
makePage("art", func () { turnPage(art) }))
list.Collapse(96, 0)
container.Adopt(list, false)
container.Adopt(list)
turnPage(intro)
window.OnClose(tomo.Stop)

View File

@ -3,10 +3,8 @@ package main
import "fmt"
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -16,8 +14,9 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256))
window.SetTitle("Main")
container := containers.NewContainer(layouts.Vertical { true, true })
container.Adopt(elements.NewLabel("Main window", false), true)
container := elements.NewVBox (
elements.SpaceBoth,
elements.NewLabel("Main window"))
window.Adopt(container)
window.OnClose(tomo.Stop)
@ -33,8 +32,9 @@ func createPanel (parent tomo.MainWindow, id int, bounds image.Rectangle) {
window, _ := parent.NewPanel(bounds)
title := fmt.Sprint("Panel #", id)
window.SetTitle(title)
container := containers.NewContainer(layouts.Vertical { true, true })
container.Adopt(elements.NewLabel(title, false), true)
container := elements.NewVBox (
elements.SpaceBoth,
elements.NewLabel(title))
window.Adopt(container)
window.Show()
}

View File

@ -2,10 +2,8 @@ package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -16,10 +14,10 @@ func run () {
if err != nil { panic(err.Error()) }
window.SetTitle("Dialog Boxes")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
container.Adopt(elements.NewLabel("Try out different dialogs:", false), true)
container.AdoptExpand(elements.NewLabel("Try out different dialogs:"))
infoButton := elements.NewButton("popups.DialogKindInfo")
infoButton.OnClick (func () {
@ -29,7 +27,7 @@ func run () {
"Information",
"You are wacky")
})
container.Adopt(infoButton, false)
container.Adopt(infoButton)
infoButton.Focus()
questionButton := elements.NewButton("popups.DialogKindQuestion")
@ -43,7 +41,7 @@ func run () {
popups.Button { "No", func () { } },
popups.Button { "Not sure", func () { } })
})
container.Adopt(questionButton, false)
container.Adopt(questionButton)
warningButton := elements.NewButton("popups.DialogKindWarning")
warningButton.OnClick (func () {
@ -53,7 +51,7 @@ func run () {
"Warning",
"They are fast approaching.")
})
container.Adopt(warningButton, false)
container.Adopt(warningButton)
errorButton := elements.NewButton("popups.DialogKindError")
errorButton.OnClick (func () {
@ -63,22 +61,23 @@ func run () {
"Error",
"There is nowhere left to go.")
})
container.Adopt(errorButton, false)
container.Adopt(errorButton)
menuButton := elements.NewButton("menu")
menuButton.OnClick (func () {
// TODO: make a better way to get the bounds of something
menu, err := window.NewMenu (
tomo.Bounds(0, 0, 64, 64).
Add(menuButton.Bounds().Min))
Add(menuButton.Entity().Bounds().Min))
if err != nil { println(err.Error()) }
menu.Adopt(elements.NewLabel("I'm a shy window...", true))
menu.Adopt(elements.NewLabelWrapped("I'm a shy window..."))
menu.Show()
})
container.Adopt(menuButton, false)
container.Adopt(menuButton)
cancelButton := elements.NewButton("No thank you.")
cancelButton.OnClick(tomo.Stop)
container.Adopt(cancelButton, false)
container.Adopt(cancelButton)
window.OnClose(tomo.Stop)
window.Show()

View File

@ -3,9 +3,7 @@ package main
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
@ -15,16 +13,15 @@ func main () {
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Approaching")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
container.Adopt (elements.NewLabel (
"Rapidly approaching your location...", false), false)
container.AdoptExpand(elements.NewLabel("Rapidly approaching your location..."))
bar := elements.NewProgressBar(0)
container.Adopt(bar, false)
container.Adopt(bar)
button := elements.NewButton("Stop")
button.SetEnabled(false)
container.Adopt(button, false)
container.Adopt(button)
window.OnClose(tomo.Stop)
window.Show()

View File

@ -1,9 +1,7 @@
package main
import "time"
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/canvas"
type Game struct {
*Raycaster
@ -31,21 +29,17 @@ func NewGame (world World, textures Textures) (game *Game) {
return
}
func (game *Game) DrawTo (
canvas canvas.Canvas,
bounds image.Rectangle,
onDamage func (image.Rectangle),
) {
if canvas == nil {
select {
case game.stopChan <- true:
default:
}
} else if !game.running {
game.running = true
go game.run()
}
game.Raycaster.DrawTo(canvas, bounds, onDamage)
func (game *Game) Start () {
if game.running == true { return }
game.running = true
go game.run()
}
func (game *Game) Stop () {
select {
case game.stopChan <- true:
default:
}
}
func (game *Game) Stamina () float64 {
@ -109,8 +103,8 @@ func (game *Game) tick () {
if game.stamina < 0 {
game.stamina = 0
}
tomo.Do(game.Draw)
tomo.Do(game.Invalidate)
if statUpdate && game.onStatUpdate != nil {
tomo.Do(game.onStatUpdate)
}

View File

@ -5,10 +5,8 @@ import _ "embed"
import _ "image/png"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
//go:embed wall.png
var wallTextureBytes []uint8
@ -17,11 +15,13 @@ func main () {
tomo.Run(run)
}
// FIXME this entire example seems to be broken
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480))
window.SetTitle("Raycaster")
container := containers.NewContainer(layouts.Vertical { false, false })
container := elements.NewVBox(elements.SpaceNone)
window.Adopt(container)
wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes))
@ -48,21 +48,22 @@ func run () {
wallTexture,
})
topBar := containers.NewContainer(layouts.Horizontal { true, true })
topBar := elements.NewHBox(elements.SpaceBoth)
staminaBar := elements.NewProgressBar(game.Stamina())
healthBar := elements.NewProgressBar(game.Health())
topBar.Adopt(elements.NewLabel("Stamina:", false), false)
topBar.Adopt(staminaBar, true)
topBar.Adopt(elements.NewLabel("Health:", false), false)
topBar.Adopt(healthBar, true)
container.Adopt(topBar, false)
container.Adopt(game, true)
topBar.Adopt(elements.NewLabel("Stamina:"))
topBar.AdoptExpand(staminaBar)
topBar.Adopt(elements.NewLabel("Health:"))
topBar.AdoptExpand(healthBar)
container.Adopt(topBar)
container.AdoptExpand(game.Raycaster)
game.Focus()
game.OnStatUpdate (func () {
staminaBar.SetProgress(game.Stamina())
})
game.Start()
window.OnClose(tomo.Stop)
window.Show()

View File

@ -4,10 +4,11 @@ package main
import "math"
import "image"
import "image/color"
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/artist/shapes"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
import "git.tebibyte.media/sashakoshka/tomo/default/config"
type ControlState struct {
@ -21,10 +22,8 @@ type ControlState struct {
}
type Raycaster struct {
*core.Core
*core.FocusableCore
core core.CoreControl
focusableControl core.FocusableCoreControl
entity tomo.FocusableEntity
config config.Wrapped
Camera
@ -49,31 +48,107 @@ func NewRaycaster (world World, textures Textures) (element *Raycaster) {
textures: textures,
renderDistance: 8,
}
element.Core, element.core = core.NewCore(element, element.drawAll)
element.FocusableCore,
element.focusableControl = core.NewFocusableCore(element.core, element.Draw)
element.core.SetMinimumSize(64, 64)
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
element.entity.SetMinimumSize(64, 64)
return
}
func (element *Raycaster) Entity () tomo.Entity {
return element.entity
}
func (element *Raycaster) Draw (destination canvas.Canvas) {
bounds := element.entity.Bounds()
// artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds)
width := bounds.Dx()
height := bounds.Dy()
halfway := bounds.Max.Y - height / 2
ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 }
for x := 0; x < width; x ++ {
ray.X = element.Camera.X
ray.Y = element.Camera.Y
distance, hitPoint, wall, horizontal := ray.Cast (
element.world, element.renderDistance)
distance *= math.Cos(ray.Angle - element.Camera.Angle)
textureX := math.Mod(hitPoint.X + hitPoint.Y, 1)
if textureX < 0 { textureX += 1 }
wallHeight := height
if distance > 0 {
wallHeight = int((float64(height) / 2.0) / float64(distance))
}
shade := 1.0
if horizontal {
shade *= 0.8
}
shade *= 1 - distance / float64(element.renderDistance)
if shade < 0 { shade = 0 }
ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF }
floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF }
// draw
data, stride := destination.Buffer()
wallStart := halfway - wallHeight
wallEnd := halfway + wallHeight
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
switch {
case y < wallStart:
data[y * stride + x + bounds.Min.X] = ceilingColor
case y < wallEnd:
textureY :=
float64(y - halfway) /
float64(wallEnd - wallStart) + 0.5
// fmt.Println(textureY)
wallColor := element.textures.At (wall, Vector {
textureX,
textureY,
})
wallColor = shadeColor(wallColor, shade)
data[y * stride + x + bounds.Min.X] = wallColor
default:
data[y * stride + x + bounds.Min.X] = floorColor
}
}
// increment angle
ray.Angle += element.Camera.Fov / float64(width)
}
// element.drawMinimap()
}
func (element *Raycaster) Invalidate () {
element.entity.Invalidate()
}
func (element *Raycaster) OnControlStateChange (callback func (ControlState)) {
element.onControlStateChange = callback
}
func (element *Raycaster) Draw () {
if element.core.HasImage() {
element.drawAll()
element.core.DamageAll()
}
func (element *Raycaster) Focus () {
element.entity.Focus()
}
func (element *Raycaster) SetEnabled (bool) { }
func (element *Raycaster) Enabled () bool { return true }
func (element *Raycaster) HandleFocusChange () { }
func (element *Raycaster) HandleMouseDown (x, y int, button input.Button) {
if !element.Focused() { element.Focus() }
element.entity.Focus()
}
func (element *Raycaster) HandleMouseUp (x, y int, button input.Button) { }
func (element *Raycaster) HandleMouseMove (x, y int) { }
func (element *Raycaster) HandleMouseScroll (x, y int, deltaX, deltaY float64) { }
func (element *Raycaster) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
switch key {
@ -109,75 +184,6 @@ func (element *Raycaster) HandleKeyUp(key input.Key, modifiers input.Modifiers)
}
}
func (element *Raycaster) drawAll () {
bounds := element.Bounds()
// artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds)
width := bounds.Dx()
height := bounds.Dy()
halfway := bounds.Max.Y - height / 2
ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 }
for x := 0; x < width; x ++ {
ray.X = element.Camera.X
ray.Y = element.Camera.Y
distance, hitPoint, wall, horizontal := ray.Cast (
element.world, element.renderDistance)
distance *= math.Cos(ray.Angle - element.Camera.Angle)
textureX := math.Mod(hitPoint.X + hitPoint.Y, 1)
if textureX < 0 { textureX += 1 }
wallHeight := height
if distance > 0 {
wallHeight = int((float64(height) / 2.0) / float64(distance))
}
shade := 1.0
if horizontal {
shade *= 0.8
}
shade *= 1 - distance / float64(element.renderDistance)
if shade < 0 { shade = 0 }
ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF }
floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF }
// draw
data, stride := element.core.Buffer()
wallStart := halfway - wallHeight
wallEnd := halfway + wallHeight
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
switch {
case y < wallStart:
data[y * stride + x + bounds.Min.X] = ceilingColor
case y < wallEnd:
textureY :=
float64(y - halfway) /
float64(wallEnd - wallStart) + 0.5
// fmt.Println(textureY)
wallColor := element.textures.At (wall, Vector {
textureX,
textureY,
})
wallColor = shadeColor(wallColor, shade)
data[y * stride + x + bounds.Min.X] = wallColor
default:
data[y * stride + x + bounds.Min.X] = floorColor
}
}
// increment angle
ray.Angle += element.Camera.Fov / float64(width)
}
// element.drawMinimap()
}
func shadeColor (c color.RGBA, brightness float64) color.RGBA {
return color.RGBA {
uint8(float64(c.R) * brightness),
@ -187,8 +193,8 @@ func shadeColor (c color.RGBA, brightness float64) color.RGBA {
}
}
func (element *Raycaster) drawMinimap () {
bounds := element.Bounds()
func (element *Raycaster) drawMinimap (destination canvas.Canvas) {
bounds := element.entity.Bounds()
scale := 8
for y := 0; y < len(element.world.Data) / element.world.Stride; y ++ {
for x := 0; x < element.world.Stride; x ++ {
@ -204,7 +210,7 @@ func (element *Raycaster) drawMinimap () {
cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF }
}
shapes.FillColorRectangle (
element.core,
destination,
cellColor,
cellBounds.Inset(1))
}}
@ -219,16 +225,16 @@ func (element *Raycaster) drawMinimap () {
playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8)
shapes.FillColorEllipse (
element.core,
destination,
artist.Hex(0xFFFFFFFF),
playerBounds)
shapes.ColorLine (
element.core,
destination,
artist.Hex(0xFFFFFFFF), 1,
playerPt,
playerAnglePt)
shapes.ColorLine (
element.core,
destination,
artist.Hex(0x00FF00FF), 1,
playerPt,
hitPt)

View File

@ -2,10 +2,8 @@ package main
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -14,37 +12,39 @@ func main () {
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 240))
window.SetTitle("Scroll")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
textBox := elements.NewTextBox("", copypasta)
scrollContainer := containers.NewScrollContainer(true, false)
disconnectedContainer := containers.NewContainer (layouts.Horizontal {
Gap: true,
})
disconnectedContainer := elements.NewHBox(elements.SpaceMargin)
list := elements.NewList (
elements.NewListEntry("This is list item 0", nil),
elements.NewListEntry("This is list item 1", nil),
elements.NewListEntry("This is list item 2", nil),
elements.NewListEntry("This is list item 3", nil),
elements.NewListEntry("This is list item 4", nil),
elements.NewListEntry("This is list item 5", nil),
elements.NewListEntry("This is list item 6", nil),
elements.NewListEntry("This is list item 7", nil),
elements.NewListEntry("This is list item 8", nil),
elements.NewListEntry("This is list item 9", nil),
elements.NewListEntry("This is list item 10", nil),
elements.NewListEntry("This is list item 11", nil),
elements.NewListEntry("This is list item 12", nil),
elements.NewListEntry("This is list item 13", nil),
elements.NewListEntry("This is list item 14", nil),
elements.NewListEntry("This is list item 15", nil),
elements.NewListEntry("This is list item 16", nil),
elements.NewListEntry("This is list item 17", nil),
elements.NewListEntry("This is list item 18", nil),
elements.NewListEntry("This is list item 19", nil),
elements.NewListEntry("This is list item 20", nil))
2,
elements.NewCell(elements.NewCheckbox("Item 0", true)),
elements.NewCell(elements.NewCheckbox("Item 1", false)),
elements.NewCell(elements.NewCheckbox("Item 2", false)),
elements.NewCell(elements.NewCheckbox("Item 3", true)),
elements.NewCell(elements.NewCheckbox("Item 4", false)),
elements.NewCell(elements.NewCheckbox("Item 5", false)),
elements.NewCell(elements.NewCheckbox("Item 6", false)),
elements.NewCell(elements.NewCheckbox("Item 7", true)),
elements.NewCell(elements.NewCheckbox("Item 8", true)),
elements.NewCell(elements.NewCheckbox("Item 9", false)),
elements.NewCell(elements.NewCheckbox("Item 10", false)),
elements.NewCell(elements.NewCheckbox("Item 11", true)),
elements.NewCell(elements.NewCheckbox("Item 12", false)),
elements.NewCell(elements.NewCheckbox("Item 13", true)),
elements.NewCell(elements.NewCheckbox("Item 14", false)),
elements.NewCell(elements.NewCheckbox("Item 15", false)),
elements.NewCell(elements.NewCheckbox("Item 16", true)),
elements.NewCell(elements.NewCheckbox("Item 17", true)),
elements.NewCell(elements.NewCheckbox("Item 18", false)),
elements.NewCell(elements.NewCheckbox("Item 19", false)),
elements.NewCell(elements.NewCheckbox("Item 20", true)),
elements.NewCell(elements.NewCheckbox("Item 21", false)),
elements.NewCell(elements.NewScroll (
elements.ScrollHorizontal,
elements.NewTextBox("", "I bet you weren't expecting this!"))))
list.Collapse(0, 32)
scrollBar := elements.NewScrollBar(true)
list.OnScrollBoundsChange (func () {
@ -56,17 +56,16 @@ func run () {
list.ScrollTo(viewport)
})
scrollContainer.Adopt(textBox)
container.Adopt(elements.NewLabel("A ScrollContainer:", false), false)
container.Adopt(scrollContainer, false)
disconnectedContainer.Adopt(list, false)
disconnectedContainer.Adopt (elements.NewLabel (
container.Adopt(elements.NewLabel("A ScrollContainer:"))
container.Adopt(elements.NewScroll(elements.ScrollHorizontal, textBox))
disconnectedContainer.Adopt(list)
disconnectedContainer.AdoptExpand(elements.NewLabelWrapped (
"Notice how the scroll bar to the right can be used to " +
"control the list, despite not even touching it. It is " +
"indeed a thing you can do. It is also terrible UI design so " +
"don't do it.", true), true)
disconnectedContainer.Adopt(scrollBar, false)
container.Adopt(disconnectedContainer, true)
"don't do it."))
disconnectedContainer.Adopt(scrollBar)
container.AdoptExpand(disconnectedContainer)
window.OnClose(tomo.Stop)
window.Show()

View File

@ -1,10 +1,8 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -14,15 +12,15 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Spaced Out")
container := containers.NewContainer(layouts.Vertical { true, true })
window.Adopt(container)
container.Adopt (elements.NewLabel("This is at the top", false), false)
container.Adopt (elements.NewSpacer(true), false)
container.Adopt (elements.NewLabel("This is in the middle", false), false)
container.Adopt (elements.NewSpacer(false), true)
container.Adopt (elements.NewLabel("This is at the bottom", false), false)
container := elements.NewVBox (
elements.SpaceBoth,
elements.NewLabel("This is at the top"),
elements.NewLine(),
elements.NewLabel("This is in the middle"))
container.AdoptExpand(elements.NewSpacer())
container.Adopt(elements.NewLabel("This is at the bottom"))
window.Adopt(container)
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,10 +1,8 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
func main () {
tomo.Run(run)
@ -14,12 +12,12 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
window.SetTitle("Switches")
container := containers.NewContainer(layouts.Vertical { true, true })
container := elements.NewVBox(elements.SpaceBoth)
window.Adopt(container)
container.Adopt(elements.NewSwitch("hahahah", false), false)
container.Adopt(elements.NewSwitch("hehehehheheh", false), false)
container.Adopt(elements.NewSwitch("you can flick da swicth", false), false)
container.Adopt(elements.NewSwitch("hahahah", false))
container.Adopt(elements.NewSwitch("hehehehheheh", false))
container.Adopt(elements.NewSwitch("you can flick da swicth", false))
window.OnClose(tomo.Stop)
window.Show()

View File

@ -1,17 +0,0 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
tomo.Run(run)
}
func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128))
window.SetTitle("hellorld!")
window.Adopt(testing.NewMouse())
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,10 +1,8 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
func main () {
@ -15,26 +13,25 @@ func run () {
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128))
window.SetTitle("vertical stack")
container := containers.NewContainer(layouts.Vertical { true, true })
window.Adopt(container)
container := elements.NewVBox(elements.SpaceBoth)
label := elements.NewLabel("it is a label hehe", true)
label := elements.NewLabelWrapped("it is a label hehe")
button := elements.NewButton("drawing pad")
okButton := elements.NewButton("OK")
button.OnClick (func () {
container.DisownAll()
container.Adopt(elements.NewLabel("Draw here:", false), false)
container.Adopt(testing.NewMouse(), true)
container.Adopt(okButton, false)
container.Adopt(elements.NewLabel("Draw here (not really):"))
container.AdoptExpand(testing.NewMouse())
container.Adopt(okButton)
okButton.Focus()
})
okButton.OnClick(tomo.Stop)
container.AdoptExpand(label)
container.Adopt(button, okButton)
window.Adopt(container)
container.Adopt(label, true)
container.Adopt(button, false)
container.Adopt(okButton, false)
okButton.Focus()
window.OnClose(tomo.Stop)
window.Show()
}

View File

@ -1,37 +0,0 @@
package tomo
import "image"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// LayoutEntry associates an element with layout and positioning information so
// it can be arranged by a Layout.
type LayoutEntry struct {
Element
Bounds image.Rectangle
Expand bool
}
// Layout is capable of arranging elements within a container. It is also able
// to determine the minimum amount of room it needs to do so.
type Layout interface {
// Arrange takes in a slice of entries and a bounding width and height,
// and changes the position of the entiries in the slice so that they
// are properly laid out. The given width and height should not be less
// than what is returned by MinimumSize.
Arrange (
entries []LayoutEntry,
margin image.Point,
padding artist.Inset,
bounds image.Rectangle,
)
// MinimumSize returns the minimum width and height that the layout
// needs to properly arrange the given slice of layout entries.
MinimumSize (
entries []LayoutEntry,
margin image.Point,
padding artist.Inset,
) (
width, height int,
)
}

View File

@ -1,152 +0,0 @@
package layouts
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/artist"
// Dialog arranges elements in the form of a dialog box. The first element is
// positioned above as the main focus of the dialog, and is set to expand
// regardless of whether it is expanding or not. The remaining elements are
// arranged at the bottom in a row called the control row, which is aligned to
// the right, the last element being the rightmost one.
type Dialog struct {
// If Mergin is true, a margin will be placed between each element.
Gap bool
// If Pad is true, there will be padding running along the inside of the
// layout's border.
Pad bool
}
// Arrange arranges a list of entries into a dialog.
func (layout Dialog) Arrange (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
bounds image.Rectangle,
) {
if layout.Pad { bounds = padding.Apply(bounds) }
controlRowWidth, controlRowHeight := 0, 0
if len(entries) > 1 {
controlRowWidth,
controlRowHeight = layout.minimumSizeOfControlRow (
entries[1:], margin, padding)
}
if len(entries) > 0 {
main := entries[0]
main.Bounds.Min = bounds.Min
mainHeight := bounds.Dy() - controlRowHeight
if layout.Gap {
mainHeight -= margin.Y
}
main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight))
entries[0] = main
}
if len(entries) > 1 {
freeSpace := bounds.Dx()
expandingElements := 0
// count the number of expanding elements and the amount of free
// space for them to collectively occupy
for index, entry := range entries[1:] {
if entry.Expand {
expandingElements ++
} else {
entryMinWidth, _ := entry.MinimumSize()
freeSpace -= entryMinWidth
}
if index > 0 && layout.Gap {
freeSpace -= margin.X
}
}
expandingElementWidth := 0
if expandingElements > 0 {
expandingElementWidth = freeSpace / expandingElements
}
// determine starting position and dimensions for control row
dot := image.Pt(bounds.Min.X, bounds.Max.Y - controlRowHeight)
if expandingElements == 0 {
dot.X = bounds.Max.X - controlRowWidth
}
// set the size and position of each element in the control row
for index, entry := range entries[1:] {
if index > 0 && layout.Gap { dot.X += margin.X }
entry.Bounds.Min = dot
entryWidth := 0
if entry.Expand {
entryWidth = expandingElementWidth
} else {
entryWidth, _ = entry.MinimumSize()
}
dot.X += entryWidth
entryBounds := entry.Bounds
if entryBounds.Dy() != controlRowHeight ||
entryBounds.Dx() != entryWidth {
entry.Bounds.Max = entryBounds.Min.Add (
image.Pt(entryWidth, controlRowHeight))
}
entries[index + 1] = entry
}
}
}
// MinimumSize returns the minimum width and height that will be needed to
// arrange the given list of entries.
func (layout Dialog) MinimumSize (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
) (
width, height int,
) {
if len(entries) > 0 {
mainChildHeight := 0
width, mainChildHeight = entries[0].MinimumSize()
height += mainChildHeight
}
if len(entries) > 1 {
if layout.Gap { height += margin.X }
additionalWidth,
additionalHeight := layout.minimumSizeOfControlRow (
entries[1:], margin, padding)
height += additionalHeight
if additionalWidth > width {
width = additionalWidth
}
}
if layout.Pad {
width += padding.Horizontal()
height += padding.Vertical()
}
return
}
func (layout Dialog) minimumSizeOfControlRow (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
) (
width, height int,
) {
for index, entry := range entries {
entryWidth, entryHeight := entry.MinimumSize()
if entryHeight > height {
height = entryHeight
}
width += entryWidth
if layout.Gap && index > 0 {
width += margin.X
}
}
return
}

View File

@ -1,2 +0,0 @@
// Package layouts provides a set of pre-made layouts.
package layouts

View File

@ -1,110 +0,0 @@
package layouts
import "image"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
// Horizontal arranges elements horizontally. Elements at the start of the entry
// list will be positioned on the left, and elements at the end of the entry
// list will positioned on the right. All elements have the same height.
type Horizontal struct {
// If Gap is true, a gap will be placed between each element.
Gap bool
// If Pad is true, there will be padding running along the inside of the
// layout's border.
Pad bool
}
// Arrange arranges a list of entries horizontally.
func (layout Horizontal) Arrange (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
bounds image.Rectangle,
) {
if layout.Pad { bounds = padding.Apply(bounds) }
// get width of expanding elements
expandingElementWidth := layout.expandingElementWidth (
entries, margin, padding, bounds.Dx())
// set the size and position of each element
dot := fixedutil.Pt(bounds.Min)
for index, entry := range entries {
if index > 0 && layout.Gap { dot.X += fixed.I(margin.X) }
entry.Bounds.Min = fixedutil.FloorPt(dot)
entryWidth := fixed.Int26_6(0)
if entry.Expand {
entryWidth = expandingElementWidth
} else {
min, _ := entry.MinimumSize()
entryWidth = fixed.I(min)
}
dot.X += entryWidth
entry.Bounds.Max = entry.Bounds.Min.Add (
image.Pt(entryWidth.Floor(), bounds.Dy()))
entries[index] = entry
}
}
// MinimumSize returns the minimum width and height that will be needed to
// arrange the given list of entries.
func (layout Horizontal) MinimumSize (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
) (
width, height int,
) {
for index, entry := range entries {
entryWidth, entryHeight := entry.MinimumSize()
if entryHeight > height {
height = entryHeight
}
width += entryWidth
if layout.Gap && index > 0 {
width += margin.X
}
}
if layout.Pad {
width += padding.Horizontal()
height += padding.Vertical()
}
return
}
func (layout Horizontal) expandingElementWidth (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
freeSpace int,
) (
width fixed.Int26_6,
) {
expandingElements := 0
// count the number of expanding elements and the amount of free space
// for them to collectively occupy
for index, entry := range entries {
if entry.Expand {
expandingElements ++
} else {
entryMinWidth, _ := entry.MinimumSize()
freeSpace -= entryMinWidth
}
if index > 0 && layout.Gap {
freeSpace -= margin.X
}
}
if expandingElements > 0 {
width = fixed.I(freeSpace) / fixed.Int26_6(expandingElements)
}
return
}

View File

@ -1,113 +0,0 @@
package layouts
import "image"
import "golang.org/x/image/math/fixed"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
// Vertical arranges elements vertically. Elements at the start of the entry
// list will be positioned at the top, and elements at the end of the entry list
// will positioned at the bottom. All elements have the same width.
type Vertical struct {
// If Gap is true, a gap will be placed between each element.
Gap bool
// If Pad is true, there will be padding running along the inside of the
// layout's border.
Pad bool
}
// Arrange arranges a list of entries vertically.
func (layout Vertical) Arrange (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
bounds image.Rectangle,
) {
if layout.Pad { bounds = padding.Apply(bounds) }
// get height of expanding elements
expandingElementHeight, minimumHeights := layout.expandingElementHeight (
entries, margin, padding, bounds.Dy())
// set the size and position of each element
dot := fixedutil.Pt(bounds.Min)
for index, entry := range entries {
if index > 0 && layout.Gap { dot.Y += fixed.I(margin.Y) }
entry.Bounds.Min = fixedutil.FloorPt(dot)
entryHeight := fixed.Int26_6(0)
if entry.Expand {
entryHeight = expandingElementHeight
} else {
entryHeight = fixed.I(minimumHeights[index])
}
dot.Y += entryHeight
entryBounds := entry.Bounds
entry.Bounds.Max = entryBounds.Min.Add (
image.Pt(bounds.Dx(),
entryHeight.Floor()))
entries[index] = entry
}
}
// MinimumSize returns the minimum width and height that will be needed to
// arrange the given list of entries.
func (layout Vertical) MinimumSize (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
) (
width, height int,
) {
for index, entry := range entries {
entryWidth, entryHeight := entry.MinimumSize()
if entryWidth > width {
width = entryWidth
}
height += entryHeight
if layout.Gap && index > 0 {
height += margin.Y
}
}
if layout.Pad {
width += padding.Horizontal()
height += padding.Vertical()
}
return
}
func (layout Vertical) expandingElementHeight (
entries []tomo.LayoutEntry,
margin image.Point,
padding artist.Inset,
freeSpace int,
) (
height fixed.Int26_6,
minimumHeights []int,
) {
// count the number of expanding elements and the amount of free space
// for them to collectively occupy, while gathering minimum heights.
minimumHeights = make([]int, len(entries))
expandingElements := 0
for index, entry := range entries {
_, entryMinHeight := entry.MinimumSize()
minimumHeights[index] = entryMinHeight
if entry.Expand {
expandingElements ++
} else {
freeSpace -= entryMinHeight
}
if index > 0 && layout.Gap {
freeSpace -= margin.Y
}
}
if expandingElements > 0 {
height = fixed.I(freeSpace) / fixed.Int26_6(expandingElements)
}
return
}

View File

@ -1,71 +0,0 @@
package tomo
import "image"
// Parent represents a type capable of containing child elements.
type Parent interface {
// NotifyMinimumSizeChange notifies the container that a child element's
// minimum size has changed. This method is expected to be called by
// child elements when their minimum size changes.
NotifyMinimumSizeChange (child Element)
// Window returns the window containing the parent.
Window () Window
}
// FocusableParent represents a parent with keyboard navigation support.
type FocusableParent interface {
Parent
// RequestFocus notifies the parent that a child element is requesting
// keyboard focus. If the parent grants the request, the method will
// return true and the child element should behave as if a HandleFocus
// call was made.
RequestFocus (child Focusable) (granted bool)
// RequestFocusMotion notifies the parent that a child element wants the
// focus to be moved to the next focusable element.
RequestFocusNext (child Focusable)
// RequestFocusMotion notifies the parent that a child element wants the
// focus to be moved to the previous focusable element.
RequestFocusPrevious (child Focusable)
}
// FlexibleParent represents a parent that accounts for elements with
// flexible height.
type FlexibleParent interface {
Parent
// NotifyFlexibleHeightChange notifies the parent that the parameters
// affecting a child's flexible height have changed. This method is
// expected to be called by flexible child element when their content
// changes.
NotifyFlexibleHeightChange (child Flexible)
}
// ScrollableParent represents a parent that can change the scroll
// position of its child element(s).
type ScrollableParent interface {
Parent
// NotifyScrollBoundsChange notifies the parent that a child's scroll
// content bounds or viewport bounds have changed. This is expected to
// be called by child elements when they change their supported scroll
// axes, their scroll position (either autonomously or as a result of a
// call to ScrollTo()), or their content size.
NotifyScrollBoundsChange (child Scrollable)
}
// BackgroundParent represents a parent that is able to re-draw a portion of its
// background upon request. This is intended to be used by transparent elements
// that want to adopt their parent's background pattern. If a parent implements
// this interface, it should call a child's DrawTo method when its area of the
// background is affected.
type BackgroundParent interface {
Parent
// DrawBackground draws a portion of the parent's background pattern
// within the specified bounds. The parent will not push these changes.
DrawBackground (bounds image.Rectangle)
}

View File

@ -2,9 +2,7 @@ package popups
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements"
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
// DialogKind defines the semantic role of a dialog window.
type DialogKind int
@ -16,6 +14,8 @@ const (
DialogKindError
)
// TODO: add ability to have an icon for buttons
// Button represents a dialog response button.
type Button struct {
// Name contains the text to display on the button.
@ -42,11 +42,11 @@ func NewDialog (
window, _ = parent.NewModal(image.Rectangle { })
}
window.SetTitle(title)
box := elements.NewVBox(elements.SpaceBoth)
messageRow := elements.NewHBox(elements.SpaceMargin)
controlRow := elements.NewHBox(elements.SpaceMargin)
container := containers.NewContainer(layouts.Dialog { true, true })
window.Adopt(container)
messageContainer := containers.NewContainer(layouts.Horizontal { true, false })
iconId := tomo.IconInformation
switch kind {
case DialogKindInfo: iconId = tomo.IconInformation
@ -55,15 +55,19 @@ func NewDialog (
case DialogKindError: iconId = tomo.IconError
}
messageContainer.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge), false)
messageContainer.Adopt(elements.NewLabel(message, false), true)
container.Adopt(messageContainer, true)
messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge))
messageRow.AdoptExpand(elements.NewLabel(message))
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)
container.Adopt(button, false)
controlRow.Adopt(button)
button.Focus()
} else {
var button *elements.Button
@ -74,7 +78,7 @@ func NewDialog (
buttonDescriptor.OnPress()
window.Close()
})
container.Adopt(button, false)
controlRow.Adopt(button)
}
button.Focus()
}

View File

@ -197,8 +197,8 @@ const (
IconBackward
IconForward
IconRefresh
IconUpward
IconRefresh
IconYes
IconNo

View File

@ -29,6 +29,12 @@ func Do (callback func ()) {
backend.Do(callback)
}
// NewEntity generates an entity for an element using the current backend.
func NewEntity (owner Element) Entity {
assertBackend()
return backend.NewEntity(owner)
}
// NewWindow creates a new window using the current backend, and returns it as a
// MainWindow. If the window could not be created, an error is returned
// explaining why.

View File

@ -15,9 +15,6 @@ type Window interface {
// these at one time.
Adopt (Element)
// Child returns the root element of the window.
Child () Element
// SetTitle sets the title that appears on the window's title bar. This
// method might have no effect with some backends.
SetTitle (string)