Merge pull request 'ecs' (#15) from ecs into main
Reviewed-on: sashakoshka/tomo#15
This commit is contained in:
commit
1323a6c1ca
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
137
element.go
137
element.go
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package fileElements
|
||||
package elements
|
||||
|
||||
import "os"
|
||||
import "io/fs"
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
600
elements/list.go
600
elements/list.go
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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())))
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 ()
|
||||
}
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
37
layout.go
37
layout.go
|
@ -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,
|
||||
)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -1,2 +0,0 @@
|
|||
// Package layouts provides a set of pre-made layouts.
|
||||
package layouts
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
71
parent.go
71
parent.go
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
|
|
2
theme.go
2
theme.go
|
@ -197,8 +197,8 @@ const (
|
|||
|
||||
IconBackward
|
||||
IconForward
|
||||
IconRefresh
|
||||
IconUpward
|
||||
IconRefresh
|
||||
|
||||
IconYes
|
||||
IconNo
|
||||
|
|
6
tomo.go
6
tomo.go
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
Reference in New Issue