ecs #15
@@ -17,6 +17,9 @@ type Backend interface {
|
|||||||
// possible. This method must be safe to call from other threads.
|
// possible. This method must be safe to call from other threads.
|
||||||
Do (callback func ())
|
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
|
// NewWindow creates a new window within the specified bounding
|
||||||
// rectangle. The position on screen may be overridden by the backend or
|
// rectangle. The position on screen may be overridden by the backend or
|
||||||
// operating system.
|
// operating system.
|
||||||
|
|||||||
278
backends/x/entity.go
Normal file
278
backends/x/entity.go
Normal file
@@ -0,0 +1,278 @@
|
|||||||
|
package x
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
|
||||||
|
type entity struct {
|
||||||
|
window *window
|
||||||
|
parent *entity
|
||||||
|
children []*entity
|
||||||
|
element tomo.Element
|
||||||
|
|
||||||
|
bounds image.Rectangle
|
||||||
|
clippedBounds image.Rectangle
|
||||||
|
minWidth int
|
||||||
|
minHeight int
|
||||||
|
|
||||||
|
selected bool
|
||||||
|
layoutInvalid bool
|
||||||
|
isContainer bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity {
|
||||||
|
entity := &entity { element: owner }
|
||||||
|
if _, ok := owner.(tomo.Container); ok {
|
||||||
|
entity.isContainer = true
|
||||||
|
entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *entity) unlink () {
|
||||||
|
ent.propagate (func (child *entity) bool {
|
||||||
|
if child.window != nil {
|
||||||
|
delete(ent.window.system.drawingInvalid, child)
|
||||||
|
}
|
||||||
|
child.window = nil
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if ent.window != nil {
|
||||||
|
delete(ent.window.system.drawingInvalid, ent)
|
||||||
|
}
|
||||||
|
ent.parent = nil
|
||||||
|
ent.window = nil
|
||||||
|
|
||||||
|
if element, ok := ent.element.(tomo.Selectable); ok {
|
||||||
|
ent.selected = false
|
||||||
|
element.HandleSelectionChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) link (parent *entity) {
|
||||||
|
entity.parent = parent
|
||||||
|
entity.clip(parent.clippedBounds)
|
||||||
|
if parent.window != nil {
|
||||||
|
entity.setWindow(parent.window)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *entity) setWindow (window *window) {
|
||||||
|
ent.window = window
|
||||||
|
ent.Invalidate()
|
||||||
|
ent.InvalidateLayout()
|
||||||
|
ent.propagate (func (child *entity) bool {
|
||||||
|
child.window = window
|
||||||
|
ent.Invalidate()
|
||||||
|
ent.InvalidateLayout()
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) propagate (callback func (*entity) bool) bool {
|
||||||
|
for _, child := range entity.children {
|
||||||
|
if !child.propagate(callback) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return callback(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) childAt (point image.Point) *entity {
|
||||||
|
for _, child := range entity.children {
|
||||||
|
if point.In(child.bounds) {
|
||||||
|
return child.childAt(point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) scrollTargetChildAt (point image.Point) *entity {
|
||||||
|
for _, child := range entity.children {
|
||||||
|
if point.In(child.bounds) {
|
||||||
|
result := child.scrollTargetChildAt(point)
|
||||||
|
if result != nil { return result }
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := entity.element.(tomo.ScrollTarget); ok {
|
||||||
|
return entity
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer, tomo.Element)) {
|
||||||
|
if entity.parent == nil { return }
|
||||||
|
if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok {
|
||||||
|
callback(parent, entity.element)
|
||||||
|
}
|
||||||
|
entity.parent.forMouseTargetContainers(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) clip (bounds image.Rectangle) {
|
||||||
|
entity.clippedBounds = entity.bounds.Intersect(bounds)
|
||||||
|
for _, child := range entity.children {
|
||||||
|
child.clip(entity.clippedBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------- Entity ----------- //
|
||||||
|
|
||||||
|
func (entity *entity) Invalidate () {
|
||||||
|
if entity.window == nil { return }
|
||||||
|
if entity.window.system.invalidateIgnore { return }
|
||||||
|
entity.window.drawingInvalid.Add(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) Bounds () image.Rectangle {
|
||||||
|
return entity.bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) Window () tomo.Window {
|
||||||
|
return entity.window
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) SetMinimumSize (width, height int) {
|
||||||
|
entity.minWidth = width
|
||||||
|
entity.minHeight = height
|
||||||
|
if entity.parent == nil {
|
||||||
|
if entity.window != nil {
|
||||||
|
entity.window.setMinimumSize(width, height)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
entity.parent.element.(tomo.Container).
|
||||||
|
HandleChildMinimumSizeChange(entity.element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) DrawBackground (destination canvas.Canvas) {
|
||||||
|
if entity.parent != nil {
|
||||||
|
entity.parent.element.(tomo.Container).DrawBackground(destination)
|
||||||
|
} else if entity.window != nil {
|
||||||
|
entity.window.system.theme.Pattern (
|
||||||
|
tomo.PatternBackground,
|
||||||
|
tomo.State { }).Draw (
|
||||||
|
destination,
|
||||||
|
entity.window.canvas.Bounds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------- ContainerEntity ----------- //
|
||||||
|
|
||||||
|
func (entity *entity) InvalidateLayout () {
|
||||||
|
if entity.window == nil { return }
|
||||||
|
if !entity.isContainer { return }
|
||||||
|
entity.layoutInvalid = true
|
||||||
|
entity.window.system.anyLayoutInvalid = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *entity) Adopt (child tomo.Element) {
|
||||||
|
childEntity, ok := child.Entity().(*entity)
|
||||||
|
if !ok || childEntity == nil { return }
|
||||||
|
childEntity.link(ent)
|
||||||
|
ent.children = append(ent.children, childEntity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ent *entity) Insert (index int, child tomo.Element) {
|
||||||
|
childEntity, ok := child.Entity().(*entity)
|
||||||
|
if !ok || childEntity == nil { return }
|
||||||
|
ent.children = append (
|
||||||
|
ent.children[:index + 1],
|
||||||
|
ent.children[index:]...)
|
||||||
|
ent.children[index] = childEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) Disown (index int) {
|
||||||
|
entity.children[index].unlink()
|
||||||
|
entity.children = append (
|
||||||
|
entity.children[:index],
|
||||||
|
entity.children[index + 1:]...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) IndexOf (child tomo.Element) int {
|
||||||
|
for index, childEntity := range entity.children {
|
||||||
|
if childEntity.element == child {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) Child (index int) tomo.Element {
|
||||||
|
return entity.children[index].element
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) CountChildren () int {
|
||||||
|
return len(entity.children)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) PlaceChild (index int, bounds image.Rectangle) {
|
||||||
|
child := entity.children[index]
|
||||||
|
child.bounds = bounds
|
||||||
|
child.clip(entity.clippedBounds)
|
||||||
|
child.Invalidate()
|
||||||
|
child.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) SelectChild (index int, selected bool) {
|
||||||
|
child := entity.children[index]
|
||||||
|
if element, ok := child.element.(tomo.Selectable); ok {
|
||||||
|
if child.selected == selected { return }
|
||||||
|
child.selected = selected
|
||||||
|
element.HandleSelectionChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) ChildMinimumSize (index int) (width, height int) {
|
||||||
|
childEntity := entity.children[index]
|
||||||
|
return childEntity.minWidth, childEntity.minHeight
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------- FocusableEntity ----------- //
|
||||||
|
|
||||||
|
func (entity *entity) Focused () bool {
|
||||||
|
if entity.window == nil { return false }
|
||||||
|
return entity.window.focused == entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) Focus () {
|
||||||
|
if entity.window == nil { return }
|
||||||
|
entity.window.system.focus(entity)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) FocusNext () {
|
||||||
|
entity.window.system.focusNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (entity *entity) FocusPrevious () {
|
||||||
|
entity.window.system.focusPrevious()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------- SelectableEntity ----------- //
|
||||||
|
|
||||||
|
func (entity *entity) Selected () bool {
|
||||||
|
return entity.selected
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------- FlexibleEntity ----------- //
|
||||||
|
|
||||||
|
func (entity *entity) NotifyFlexibleHeightChange () {
|
||||||
|
if entity.parent == nil { return }
|
||||||
|
if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok {
|
||||||
|
parent.HandleChildFlexibleHeightChange (
|
||||||
|
entity.element.(tomo.Flexible))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ----------- ScrollableEntity ----------- //
|
||||||
|
|
||||||
|
func (entity *entity) NotifyScrollBoundsChange () {
|
||||||
|
if entity.parent == nil { return }
|
||||||
|
if parent, ok := entity.parent.element.(tomo.ScrollableContainer); ok {
|
||||||
|
parent.HandleChildScrollBoundsChange (
|
||||||
|
entity.element.(tomo.Scrollable))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,28 +20,19 @@ func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) {
|
|||||||
(state & window.backend.modifierMasks.shiftLock) > 0
|
(state & window.backend.modifierMasks.shiftLock) > 0
|
||||||
if shift {
|
if shift {
|
||||||
switch button {
|
switch button {
|
||||||
case 4:
|
case 4: sum.x -= scrollDistance
|
||||||
sum.x -= scrollDistance
|
case 5: sum.x += scrollDistance
|
||||||
case 5:
|
case 6: sum.y -= scrollDistance
|
||||||
sum.x += scrollDistance
|
case 7: sum.y += scrollDistance
|
||||||
case 6:
|
|
||||||
sum.y -= scrollDistance
|
|
||||||
case 7:
|
|
||||||
sum.y += scrollDistance
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
switch button {
|
switch button {
|
||||||
case 4:
|
case 4: sum.y -= scrollDistance
|
||||||
sum.y -= scrollDistance
|
case 5: sum.y += scrollDistance
|
||||||
case 5:
|
case 6: sum.x -= scrollDistance
|
||||||
sum.y += scrollDistance
|
case 7: sum.x += scrollDistance
|
||||||
case 6:
|
|
||||||
sum.x -= scrollDistance
|
|
||||||
case 7:
|
|
||||||
sum.x += scrollDistance
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *window) handleExpose (
|
func (window *window) handleExpose (
|
||||||
@@ -49,6 +40,7 @@ func (window *window) handleExpose (
|
|||||||
event xevent.ExposeEvent,
|
event xevent.ExposeEvent,
|
||||||
) {
|
) {
|
||||||
_, region := window.compressExpose(*event.ExposeEvent)
|
_, region := window.compressExpose(*event.ExposeEvent)
|
||||||
|
window.system.afterEvent()
|
||||||
window.pushRegion(region)
|
window.pushRegion(region)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,7 +67,6 @@ func (window *window) handleConfigureNotify (
|
|||||||
configureEvent.X, configureEvent.Y,
|
configureEvent.X, configureEvent.Y,
|
||||||
configureEvent.Width, configureEvent.Height)
|
configureEvent.Width, configureEvent.Height)
|
||||||
|
|
||||||
|
|
||||||
if sizeChanged {
|
if sizeChanged {
|
||||||
configureEvent = window.compressConfigureNotify(configureEvent)
|
configureEvent = window.compressConfigureNotify(configureEvent)
|
||||||
window.updateBounds (
|
window.updateBounds (
|
||||||
@@ -85,8 +76,11 @@ func (window *window) handleConfigureNotify (
|
|||||||
window.resizeChildToFit()
|
window.resizeChildToFit()
|
||||||
|
|
||||||
if !window.exposeEventFollows(configureEvent) {
|
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,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.KeyPressEvent,
|
event xevent.KeyPressEvent,
|
||||||
) {
|
) {
|
||||||
if window.child == nil { return }
|
if window.hasModal { return }
|
||||||
if window.hasModal { return }
|
|
||||||
|
|
||||||
keyEvent := *event.KeyPressEvent
|
keyEvent := *event.KeyPressEvent
|
||||||
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State)
|
||||||
@@ -136,29 +129,25 @@ func (window *window) handleKeyPress (
|
|||||||
modifiers.NumberPad = numberPad
|
modifiers.NumberPad = numberPad
|
||||||
|
|
||||||
if key == input.KeyTab && modifiers.Alt {
|
if key == input.KeyTab && modifiers.Alt {
|
||||||
if child, ok := window.child.(tomo.Focusable); ok {
|
if modifiers.Shift {
|
||||||
direction := input.KeynavDirectionForward
|
window.system.focusPrevious()
|
||||||
if modifiers.Shift {
|
} else {
|
||||||
direction = input.KeynavDirectionBackward
|
window.system.focusNext()
|
||||||
}
|
|
||||||
|
|
||||||
if !child.HandleFocus(direction) {
|
|
||||||
child.HandleUnfocus()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if key == input.KeyEscape && window.shy {
|
} else if key == input.KeyEscape && window.shy {
|
||||||
window.Close()
|
window.Close()
|
||||||
} else if child, ok := window.child.(tomo.KeyboardTarget); ok {
|
} else if window.focused != nil {
|
||||||
child.HandleKeyDown(key, modifiers)
|
focused, ok := window.focused.element.(tomo.KeyboardTarget)
|
||||||
|
if ok { focused.HandleKeyDown(key, modifiers) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.system.afterEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *window) handleKeyRelease (
|
func (window *window) handleKeyRelease (
|
||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.KeyReleaseEvent,
|
event xevent.KeyReleaseEvent,
|
||||||
) {
|
) {
|
||||||
if window.child == nil { return }
|
|
||||||
|
|
||||||
keyEvent := *event.KeyReleaseEvent
|
keyEvent := *event.KeyReleaseEvent
|
||||||
|
|
||||||
// do not process this event if it was generated from a key repeat
|
// do not process this event if it was generated from a key repeat
|
||||||
@@ -182,8 +171,11 @@ func (window *window) handleKeyRelease (
|
|||||||
modifiers := window.modifiersFromState(keyEvent.State)
|
modifiers := window.modifiersFromState(keyEvent.State)
|
||||||
modifiers.NumberPad = numberPad
|
modifiers.NumberPad = numberPad
|
||||||
|
|
||||||
if child, ok := window.child.(tomo.KeyboardTarget); ok {
|
if window.focused != nil {
|
||||||
child.HandleKeyUp(key, modifiers)
|
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,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.ButtonPressEvent,
|
event xevent.ButtonPressEvent,
|
||||||
) {
|
) {
|
||||||
if window.child == nil { return }
|
if window.hasModal { return }
|
||||||
if window.hasModal { return }
|
|
||||||
|
|
||||||
buttonEvent := *event.ButtonPressEvent
|
buttonEvent := *event.ButtonPressEvent
|
||||||
|
point := image.Pt(int(buttonEvent.EventX), int(buttonEvent.EventY))
|
||||||
insideWindow := image.Pt (
|
insideWindow := point.In(window.canvas.Bounds())
|
||||||
int(buttonEvent.EventX),
|
scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7
|
||||||
int(buttonEvent.EventY)).In(window.canvas.Bounds())
|
|
||||||
|
|
||||||
scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7
|
|
||||||
|
|
||||||
if !insideWindow && window.shy && !scrolling {
|
if !insideWindow && window.shy && !scrolling {
|
||||||
window.Close()
|
window.Close()
|
||||||
} else if scrolling {
|
} else if scrolling {
|
||||||
if child, ok := window.child.(tomo.ScrollTarget); ok {
|
underneath := window.system.scrollTargetChildAt(point)
|
||||||
sum := scrollSum { }
|
if underneath != nil {
|
||||||
sum.add(buttonEvent.Detail, window, buttonEvent.State)
|
if child, ok := underneath.element.(tomo.ScrollTarget); ok {
|
||||||
window.compressScrollSum(buttonEvent, &sum)
|
sum := scrollSum { }
|
||||||
child.HandleScroll (
|
sum.add(buttonEvent.Detail, window, buttonEvent.State)
|
||||||
int(buttonEvent.EventX),
|
window.compressScrollSum(buttonEvent, &sum)
|
||||||
int(buttonEvent.EventY),
|
child.HandleScroll (
|
||||||
float64(sum.x), float64(sum.y))
|
point.X, point.Y,
|
||||||
|
float64(sum.x), float64(sum.y))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} 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 (
|
child.HandleMouseDown (
|
||||||
int(buttonEvent.EventX),
|
point.X, point.Y,
|
||||||
int(buttonEvent.EventY),
|
|
||||||
input.Button(buttonEvent.Detail))
|
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 (
|
func (window *window) handleButtonRelease (
|
||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.ButtonReleaseEvent,
|
event xevent.ButtonReleaseEvent,
|
||||||
) {
|
) {
|
||||||
if window.child == nil { return }
|
buttonEvent := *event.ButtonReleaseEvent
|
||||||
|
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
|
||||||
if child, ok := window.child.(tomo.MouseTarget); ok {
|
dragging := window.system.drags[buttonEvent.Detail]
|
||||||
buttonEvent := *event.ButtonReleaseEvent
|
if dragging != nil {
|
||||||
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
|
if child, ok := dragging.element.(tomo.MouseTarget); ok {
|
||||||
child.HandleMouseUp (
|
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.EventX),
|
||||||
int(buttonEvent.EventY),
|
int(buttonEvent.EventY),
|
||||||
input.Button(buttonEvent.Detail))
|
input.Button(buttonEvent.Detail),
|
||||||
|
child)
|
||||||
|
}
|
||||||
|
dragging.forMouseTargetContainers(callback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
window.system.afterEvent()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *window) handleMotionNotify (
|
func (window *window) handleMotionNotify (
|
||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.MotionNotifyEvent,
|
event xevent.MotionNotifyEvent,
|
||||||
) {
|
) {
|
||||||
if window.child == nil { return }
|
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
||||||
|
x := int(motionEvent.EventX)
|
||||||
|
y :=int(motionEvent.EventY)
|
||||||
|
|
||||||
if child, ok := window.child.(tomo.MotionTarget); ok {
|
handled := false
|
||||||
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
for _, child := range window.system.drags {
|
||||||
child.HandleMotion (
|
if child == nil { continue }
|
||||||
int(motionEvent.EventX),
|
if child, ok := child.element.(tomo.MotionTarget); ok {
|
||||||
int(motionEvent.EventY))
|
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 (
|
func (window *window) handleSelectionNotify (
|
||||||
|
|||||||
@@ -97,11 +97,13 @@ func (request *selectionRequest) convertSelection (
|
|||||||
|
|
||||||
func (request *selectionRequest) die (err error) {
|
func (request *selectionRequest) die (err error) {
|
||||||
request.callback(nil, err)
|
request.callback(nil, err)
|
||||||
|
request.window.system.afterEvent()
|
||||||
request.state = selReqStateClosed
|
request.state = selReqStateClosed
|
||||||
}
|
}
|
||||||
|
|
||||||
func (request *selectionRequest) finalize (data data.Data) {
|
func (request *selectionRequest) finalize (data data.Data) {
|
||||||
request.callback(data, nil)
|
request.callback(data, nil)
|
||||||
|
request.window.system.afterEvent()
|
||||||
request.state = selReqStateClosed
|
request.state = selReqStateClosed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
186
backends/x/system.go
Normal file
186
backends/x/system.go
Normal file
@@ -0,0 +1,186 @@
|
|||||||
|
package x
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
|
type entitySet map[*entity] struct { }
|
||||||
|
|
||||||
|
func (set entitySet) Empty () bool {
|
||||||
|
return len(set) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set entitySet) Has (entity *entity) bool {
|
||||||
|
_, ok := set[entity]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func (set entitySet) Add (entity *entity) {
|
||||||
|
set[entity] = struct { } { }
|
||||||
|
}
|
||||||
|
|
||||||
|
type system struct {
|
||||||
|
child *entity
|
||||||
|
focused *entity
|
||||||
|
canvas canvas.BasicCanvas
|
||||||
|
|
||||||
|
theme theme.Wrapped
|
||||||
|
config config.Wrapped
|
||||||
|
|
||||||
|
invalidateIgnore bool
|
||||||
|
drawingInvalid entitySet
|
||||||
|
anyLayoutInvalid bool
|
||||||
|
|
||||||
|
drags [10]*entity
|
||||||
|
|
||||||
|
pushFunc func (image.Rectangle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) initialize () {
|
||||||
|
system.drawingInvalid = make(entitySet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) SetTheme (theme tomo.Theme) {
|
||||||
|
system.theme.Theme = theme
|
||||||
|
system.propagate (func (entity *entity) bool {
|
||||||
|
if child, ok := system.child.element.(tomo.Themeable); ok {
|
||||||
|
child.SetTheme(theme)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) SetConfig (config tomo.Config) {
|
||||||
|
system.config.Config = config
|
||||||
|
system.propagate (func (entity *entity) bool {
|
||||||
|
if child, ok := system.child.element.(tomo.Configurable); ok {
|
||||||
|
child.SetConfig(config)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) focus (entity *entity) {
|
||||||
|
previous := system.focused
|
||||||
|
system.focused = entity
|
||||||
|
if previous != nil {
|
||||||
|
previous.element.(tomo.Focusable).HandleFocusChange()
|
||||||
|
}
|
||||||
|
if entity != nil {
|
||||||
|
entity.element.(tomo.Focusable).HandleFocusChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) focusNext () {
|
||||||
|
found := system.focused == nil
|
||||||
|
focused := false
|
||||||
|
system.propagate (func (entity *entity) bool {
|
||||||
|
if found {
|
||||||
|
// looking for the next element to select
|
||||||
|
child, ok := entity.element.(tomo.Focusable)
|
||||||
|
if ok && child.Enabled() {
|
||||||
|
// found it
|
||||||
|
entity.Focus()
|
||||||
|
focused = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// looking for the current focused element
|
||||||
|
if entity == system.focused {
|
||||||
|
// found it
|
||||||
|
found = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
|
||||||
|
if !focused { system.focus(nil) }
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) focusPrevious () {
|
||||||
|
var behind *entity
|
||||||
|
system.propagate (func (entity *entity) bool {
|
||||||
|
if entity == system.focused {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
child, ok := entity.element.(tomo.Focusable)
|
||||||
|
if ok && child.Enabled() { behind = entity }
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
system.focus(behind)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) propagate (callback func (*entity) bool) {
|
||||||
|
if system.child == nil { return }
|
||||||
|
system.child.propagate(callback)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) childAt (point image.Point) *entity {
|
||||||
|
if system.child == nil { return nil }
|
||||||
|
return system.child.childAt(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) scrollTargetChildAt (point image.Point) *entity {
|
||||||
|
if system.child == nil { return nil }
|
||||||
|
return system.child.scrollTargetChildAt(point)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) resizeChildToFit () {
|
||||||
|
system.child.bounds = system.canvas.Bounds()
|
||||||
|
system.child.clippedBounds = system.child.bounds
|
||||||
|
system.child.Invalidate()
|
||||||
|
if system.child.isContainer {
|
||||||
|
system.child.InvalidateLayout()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) afterEvent () {
|
||||||
|
if system.anyLayoutInvalid {
|
||||||
|
system.layout(system.child, false)
|
||||||
|
system.anyLayoutInvalid = false
|
||||||
|
}
|
||||||
|
system.draw()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) layout (entity *entity, force bool) {
|
||||||
|
if entity == nil { return }
|
||||||
|
if entity.layoutInvalid == true || force {
|
||||||
|
if element, ok := entity.element.(tomo.Layoutable); ok {
|
||||||
|
element.Layout()
|
||||||
|
entity.layoutInvalid = false
|
||||||
|
force = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range entity.children {
|
||||||
|
system.layout(child, force)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (system *system) draw () {
|
||||||
|
finalBounds := image.Rectangle { }
|
||||||
|
|
||||||
|
// ignore invalidations that result from drawing elements, because if an
|
||||||
|
// element decides to do that it really needs to rethink its life
|
||||||
|
// choices.
|
||||||
|
system.invalidateIgnore = true
|
||||||
|
defer func () { system.invalidateIgnore = false } ()
|
||||||
|
|
||||||
|
for entity := range system.drawingInvalid {
|
||||||
|
if entity.clippedBounds.Empty() { continue }
|
||||||
|
entity.element.Draw (canvas.Cut (
|
||||||
|
system.canvas,
|
||||||
|
entity.clippedBounds))
|
||||||
|
finalBounds = finalBounds.Union(entity.clippedBounds)
|
||||||
|
}
|
||||||
|
system.drawingInvalid = make(entitySet)
|
||||||
|
|
||||||
|
// TODO: don't just union all the bounds together, we can definetly
|
||||||
|
// consolidateupdated regions more efficiently than this.
|
||||||
|
if !finalBounds.Empty() {
|
||||||
|
system.pushFunc(finalBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,20 +13,16 @@ import "github.com/jezek/xgbutil/mousebind"
|
|||||||
import "github.com/jezek/xgbutil/xgraphics"
|
import "github.com/jezek/xgbutil/xgraphics"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
// import "runtime/debug"
|
|
||||||
|
|
||||||
type mainWindow struct { *window }
|
type mainWindow struct { *window }
|
||||||
type menuWindow struct { *window }
|
type menuWindow struct { *window }
|
||||||
type window struct {
|
type window struct {
|
||||||
|
system
|
||||||
|
|
||||||
backend *Backend
|
backend *Backend
|
||||||
xWindow *xwindow.Window
|
xWindow *xwindow.Window
|
||||||
xCanvas *xgraphics.Image
|
xCanvas *xgraphics.Image
|
||||||
canvas canvas.BasicCanvas
|
|
||||||
child tomo.Element
|
|
||||||
onClose func ()
|
|
||||||
skipChildDrawCallback bool
|
|
||||||
|
|
||||||
title, application string
|
title, application string
|
||||||
|
|
||||||
@@ -34,15 +30,14 @@ type window struct {
|
|||||||
hasModal bool
|
hasModal bool
|
||||||
shy bool
|
shy bool
|
||||||
|
|
||||||
theme tomo.Theme
|
|
||||||
config tomo.Config
|
|
||||||
|
|
||||||
selectionRequest *selectionRequest
|
selectionRequest *selectionRequest
|
||||||
selectionClaim *selectionClaim
|
selectionClaim *selectionClaim
|
||||||
|
|
||||||
metrics struct {
|
metrics struct {
|
||||||
bounds image.Rectangle
|
bounds image.Rectangle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onClose func ()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (backend *Backend) NewWindow (
|
func (backend *Backend) NewWindow (
|
||||||
@@ -53,6 +48,7 @@ func (backend *Backend) NewWindow (
|
|||||||
) {
|
) {
|
||||||
if backend == nil { panic("nil backend") }
|
if backend == nil { panic("nil backend") }
|
||||||
window, err := backend.newWindow(bounds, false)
|
window, err := backend.newWindow(bounds, false)
|
||||||
|
|
||||||
output = mainWindow { window }
|
output = mainWindow { window }
|
||||||
return output, err
|
return output, err
|
||||||
}
|
}
|
||||||
@@ -69,6 +65,10 @@ func (backend *Backend) newWindow (
|
|||||||
|
|
||||||
window := &window { backend: backend }
|
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)
|
window.xWindow, err = xwindow.Generate(backend.connection)
|
||||||
if err != nil { return }
|
if err != nil { return }
|
||||||
|
|
||||||
@@ -126,7 +126,7 @@ func (backend *Backend) newWindow (
|
|||||||
window.SetConfig(backend.config)
|
window.SetConfig(backend.config)
|
||||||
|
|
||||||
window.metrics.bounds = bounds
|
window.metrics.bounds = bounds
|
||||||
window.childMinimumSizeChangeCallback(8, 8)
|
window.setMinimumSize(8, 8)
|
||||||
|
|
||||||
window.reallocateCanvas()
|
window.reallocateCanvas()
|
||||||
|
|
||||||
@@ -136,69 +136,31 @@ func (backend *Backend) newWindow (
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *window) NotifyMinimumSizeChange (child tomo.Element) {
|
|
||||||
window.childMinimumSizeChangeCallback(child.MinimumSize())
|
|
||||||
}
|
|
||||||
|
|
||||||
func (window *window) Window () tomo.Window {
|
func (window *window) Window () tomo.Window {
|
||||||
return 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) {
|
func (window *window) Adopt (child tomo.Element) {
|
||||||
// disown previous child
|
// disown previous child
|
||||||
if window.child != nil {
|
if window.child != nil {
|
||||||
window.child.SetParent(nil)
|
window.child.unlink()
|
||||||
window.child.DrawTo(nil, image.Rectangle { }, nil)
|
window.child = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// adopt new child
|
||||||
if child != nil {
|
if child != nil {
|
||||||
// adopt new child
|
childEntity, ok := child.Entity().(*entity)
|
||||||
window.child = child
|
if ok && childEntity != nil {
|
||||||
child.SetParent(window)
|
window.child = childEntity
|
||||||
if newChild, ok := child.(tomo.Themeable); ok {
|
childEntity.setWindow(window)
|
||||||
newChild.SetTheme(window.theme)
|
window.setMinimumSize (
|
||||||
}
|
childEntity.minWidth,
|
||||||
if newChild, ok := child.(tomo.Configurable); ok {
|
childEntity.minHeight)
|
||||||
newChild.SetConfig(window.config)
|
window.resizeChildToFit()
|
||||||
}
|
|
||||||
if child != nil {
|
|
||||||
if !window.childMinimumSizeChangeCallback(child.MinimumSize()) {
|
|
||||||
window.resizeChildToFit()
|
|
||||||
window.redrawChildEntirely()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *window) Child () (child tomo.Element) {
|
|
||||||
child = window.child
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (window *window) SetTitle (title string) {
|
func (window *window) SetTitle (title string) {
|
||||||
window.title = title
|
window.title = title
|
||||||
ewmh.WmNameSet (
|
ewmh.WmNameSet (
|
||||||
@@ -317,43 +279,6 @@ func (window menuWindow) Pin () {
|
|||||||
// TODO iungrab keyboard and mouse
|
// 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 () {
|
func (window *window) Show () {
|
||||||
if window.child == nil {
|
if window.child == nil {
|
||||||
window.xCanvas.For (func (x, y int) xgraphics.BGRA {
|
window.xCanvas.For (func (x, y int) xgraphics.BGRA {
|
||||||
@@ -417,18 +342,41 @@ func (window *window) OnClose (callback func ()) {
|
|||||||
window.onClose = callback
|
window.onClose = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *window) SetTheme (theme tomo.Theme) {
|
func (window *window) grabInput () {
|
||||||
window.theme = theme
|
keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id)
|
||||||
if child, ok := window.child.(tomo.Themeable); ok {
|
mousebind.GrabPointer (
|
||||||
child.SetTheme(theme)
|
window.backend.connection,
|
||||||
}
|
window.xWindow.Id,
|
||||||
|
window.backend.connection.RootWin(), 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *window) SetConfig (config tomo.Config) {
|
func (window *window) ungrabInput () {
|
||||||
window.config = config
|
keybind.UngrabKeyboard(window.backend.connection)
|
||||||
if child, ok := window.child.(tomo.Configurable); ok {
|
mousebind.UngrabPointer(window.backend.connection)
|
||||||
child.SetConfig(config)
|
}
|
||||||
|
|
||||||
|
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 () {
|
func (window *window) reallocateCanvas () {
|
||||||
@@ -464,22 +412,7 @@ func (window *window) reallocateCanvas () {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *window) redrawChildEntirely () {
|
func (window *window) pasteAndPush (region image.Rectangle) {
|
||||||
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 }
|
|
||||||
window.paste(region)
|
window.paste(region)
|
||||||
window.pushRegion(region)
|
window.pushRegion(region)
|
||||||
}
|
}
|
||||||
@@ -492,7 +425,6 @@ func (window *window) paste (region image.Rectangle) {
|
|||||||
dstStride := window.xCanvas.Stride
|
dstStride := window.xCanvas.Stride
|
||||||
dstData := window.xCanvas.Pix
|
dstData := window.xCanvas.Pix
|
||||||
|
|
||||||
// debug.PrintStack()
|
|
||||||
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
for y := bounds.Min.Y; y < bounds.Max.Y; y ++ {
|
||||||
srcYComponent := y * stride
|
srcYComponent := y * stride
|
||||||
dstYComponent := y * dstStride
|
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 (
|
icccm.WmNormalHintsSet (
|
||||||
window.backend.connection,
|
window.backend.connection,
|
||||||
window.xWindow.Id,
|
window.xWindow.Id,
|
||||||
@@ -523,20 +469,5 @@ func (window *window) childMinimumSizeChangeCallback (width, height int) (resize
|
|||||||
if newWidth != window.metrics.bounds.Dx() ||
|
if newWidth != window.metrics.bounds.Dx() ||
|
||||||
newHeight != window.metrics.bounds.Dy() {
|
newHeight != window.metrics.bounds.Dy() {
|
||||||
window.xWindow.Resize(newWidth, newHeight)
|
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
|
<- pingAfter
|
||||||
case callback := <- backend.doChannel:
|
case callback := <- backend.doChannel:
|
||||||
callback()
|
callback()
|
||||||
|
for _, window := range backend.windows {
|
||||||
|
window.system.afterEvent()
|
||||||
|
}
|
||||||
case <- pingQuit:
|
case <- pingQuit:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -212,14 +212,9 @@ func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.P
|
|||||||
switch id {
|
switch id {
|
||||||
case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF)
|
case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF)
|
||||||
case tomo.PatternDead: return defaultTextures[0][offset]
|
case tomo.PatternDead: return defaultTextures[0][offset]
|
||||||
case tomo.PatternRaised:
|
case tomo.PatternRaised: return defaultTextures[1][offset]
|
||||||
if c.Match("tomo", "listEntry", "") {
|
case tomo.PatternSunken: return defaultTextures[2][offset]
|
||||||
return defaultTextures[10][offset]
|
case tomo.PatternPinboard: return defaultTextures[3][offset]
|
||||||
} else {
|
|
||||||
return defaultTextures[1][offset]
|
|
||||||
}
|
|
||||||
case tomo.PatternSunken: return defaultTextures[2][offset]
|
|
||||||
case tomo.PatternPinboard: return defaultTextures[3][offset]
|
|
||||||
case tomo.PatternButton:
|
case tomo.PatternButton:
|
||||||
switch {
|
switch {
|
||||||
case c.Match("tomo", "checkbox", ""):
|
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.
|
// Padding returns the default padding value for the given pattern.
|
||||||
func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
|
func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
|
||||||
switch id {
|
switch id {
|
||||||
case tomo.PatternRaised:
|
|
||||||
if c.Match("tomo", "listEntry", "") {
|
|
||||||
return artist.I(4, 8)
|
|
||||||
} else {
|
|
||||||
return artist.I(8)
|
|
||||||
}
|
|
||||||
case tomo.PatternSunken:
|
case tomo.PatternSunken:
|
||||||
if c.Match("tomo", "list", "") {
|
if c.Match("tomo", "progressBar", "") {
|
||||||
return artist.I(4, 0, 3)
|
|
||||||
} else if c.Match("tomo", "progressBar", "") {
|
|
||||||
return artist.I(2, 1, 1, 2)
|
return artist.I(2, 1, 1, 2)
|
||||||
} else {
|
} else {
|
||||||
return artist.I(8)
|
return artist.I(8)
|
||||||
@@ -292,16 +279,26 @@ func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset {
|
|||||||
} else {
|
} else {
|
||||||
return artist.I(8)
|
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.PatternGutter: return artist.I(0)
|
||||||
case tomo.PatternLine: return artist.I(1)
|
case tomo.PatternLine: return artist.I(1)
|
||||||
case tomo.PatternMercury: return artist.I(5)
|
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.
|
// Margin returns the default margin value for the given pattern.
|
||||||
func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point {
|
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.
|
// 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.
|
// Element represents a basic on-screen object.
|
||||||
type Element interface {
|
type Element interface {
|
||||||
// Bounds reports the element's bounding box. This must reflect the
|
// Draw causes the element to draw to the specified canvas. The bounds
|
||||||
// bounding last given to the element by DrawTo.
|
// of this canvas specify the area that is actually drawn to, while the
|
||||||
Bounds () image.Rectangle
|
// Entity bounds specify the actual area of the element.
|
||||||
|
Draw (canvas.Canvas)
|
||||||
|
|
||||||
// MinimumSize specifies the minimum amount of pixels this element's
|
// Entity returns this element's entity.
|
||||||
// width and height may be set to. If the element is given a resize
|
Entity () Entity
|
||||||
// event with dimensions smaller than this, it will use its minimum
|
|
||||||
// instead of the offending dimension(s).
|
|
||||||
MinimumSize () (width, height int)
|
|
||||||
|
|
||||||
// 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))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Focusable represents an element that has keyboard navigation support. This
|
// Layoutable represents an element that needs to perform layout calculations
|
||||||
// includes inputs, buttons, sliders, etc. as well as any elements that have
|
// before it can draw itself.
|
||||||
// children (so keyboard navigation events can be propagated downward).
|
type Layoutable interface {
|
||||||
type Focusable interface {
|
|
||||||
Element
|
Element
|
||||||
|
|
||||||
// Focused returns whether or not this element or any of its children
|
// Layout causes this element to perform a layout operation.
|
||||||
// are currently focused.
|
Layout ()
|
||||||
Focused () bool
|
}
|
||||||
|
|
||||||
// Focus focuses this element, if its parent element grants the
|
// Container represents an element capable of containing child elements.
|
||||||
// request.
|
type Container interface {
|
||||||
Focus ()
|
Element
|
||||||
|
Layoutable
|
||||||
|
|
||||||
// HandleFocus causes this element to mark itself as focused. If the
|
// DrawBackground causes the element to draw its background pattern to
|
||||||
// element does not have children, it is disabled, or there are no more
|
// the specified canvas. The bounds of this canvas specify the area that
|
||||||
// selectable children in the given direction, it should return false
|
// is actually drawn to, while the Entity bounds specify the actual area
|
||||||
// and do nothing. Otherwise, it should select itself and any children
|
// of the element.
|
||||||
// (if applicable) and return true.
|
DrawBackground (canvas.Canvas)
|
||||||
HandleFocus (direction input.KeynavDirection) (accepted bool)
|
|
||||||
|
|
||||||
// HandleDeselection causes this element to mark itself and all of its
|
// HandleChildMinimumSizeChange is called when a child's minimum size is
|
||||||
// children as unfocused.
|
// changed.
|
||||||
HandleUnfocus ()
|
HandleChildMinimumSizeChange (child Element)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enableable represents an element that can be enabled and disabled. Disabled
|
||||||
|
// elements typically appear greyed out.
|
||||||
|
type Enableable interface {
|
||||||
|
Element
|
||||||
|
|
||||||
|
// Enabled returns whether or not the element is enabled.
|
||||||
|
Enabled () bool
|
||||||
|
|
||||||
|
// SetEnabled sets whether or not the element is enabled.
|
||||||
|
SetEnabled (bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focusable represents an element that has keyboard navigation support.
|
||||||
|
type Focusable interface {
|
||||||
|
Element
|
||||||
|
Enableable
|
||||||
|
|
||||||
|
// HandleFocusChange is called when the element is focused or unfocused.
|
||||||
|
HandleFocusChange ()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
// KeyboardTarget represents an element that can receive keyboard input.
|
||||||
@@ -85,6 +106,22 @@ type MouseTarget interface {
|
|||||||
HandleMouseUp (x, y int, button input.Button)
|
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.
|
// MotionTarget represents an element that can receive mouse motion events.
|
||||||
type MotionTarget interface {
|
type MotionTarget interface {
|
||||||
Element
|
Element
|
||||||
@@ -125,6 +162,16 @@ type Flexible interface {
|
|||||||
FlexibleHeightFor (width int) int
|
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
|
// Scrollable represents an element that can be scrolled. It acts as a viewport
|
||||||
// through which its contents can be observed.
|
// through which its contents can be observed.
|
||||||
type Scrollable interface {
|
type Scrollable interface {
|
||||||
@@ -145,6 +192,16 @@ type Scrollable interface {
|
|||||||
ScrollAxes () (horizontal, vertical bool)
|
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
|
// Collapsible represents an element who's minimum width and height can be
|
||||||
// manually resized. Scrollable elements should implement this if possible.
|
// manually resized. Scrollable elements should implement this if possible.
|
||||||
type Collapsible interface {
|
type Collapsible interface {
|
||||||
|
|||||||
216
elements/box.go
Normal file
216
elements/box.go
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
|
|
||||||
|
// Space is a list of spacing configurations that can be passed to some
|
||||||
|
// containers.
|
||||||
|
type Space int; const (
|
||||||
|
SpaceNone = 0
|
||||||
|
SpacePadding = 1
|
||||||
|
SpaceMargin = 2
|
||||||
|
SpaceBoth = SpacePadding | SpaceMargin
|
||||||
|
)
|
||||||
|
|
||||||
|
// Includes returns whether a spacing value has been or'd with another spacing
|
||||||
|
// value.
|
||||||
|
func (space Space) Includes (sub Space) bool {
|
||||||
|
return (space & sub) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Box is a container that lays out its children horizontally or vertically.
|
||||||
|
// Child elements can be set to contract to their minimum size, or expand to
|
||||||
|
// fill remaining space. Boxes can be nested and used together to create more
|
||||||
|
// complex layouts.
|
||||||
|
type Box struct {
|
||||||
|
container
|
||||||
|
theme theme.Wrapped
|
||||||
|
padding bool
|
||||||
|
margin bool
|
||||||
|
vertical bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHBox creates a new horizontal box.
|
||||||
|
func NewHBox (space Space, children ...tomo.Element) (element *Box) {
|
||||||
|
element = &Box {
|
||||||
|
padding: space.Includes(SpacePadding),
|
||||||
|
margin: space.Includes(SpaceMargin),
|
||||||
|
}
|
||||||
|
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
|
||||||
|
element.minimumSize = element.updateMinimumSize
|
||||||
|
element.init()
|
||||||
|
element.theme.Case = tomo.C("tomo", "box")
|
||||||
|
element.Adopt(children...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHBox creates a new vertical box.
|
||||||
|
func NewVBox (space Space, children ...tomo.Element) (element *Box) {
|
||||||
|
element = &Box {
|
||||||
|
padding: space.Includes(SpacePadding),
|
||||||
|
margin: space.Includes(SpaceMargin),
|
||||||
|
vertical: true,
|
||||||
|
}
|
||||||
|
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
|
||||||
|
element.minimumSize = element.updateMinimumSize
|
||||||
|
element.init()
|
||||||
|
element.theme.Case = tomo.C("tomo", "box")
|
||||||
|
element.Adopt(children...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Box) Draw (destination canvas.Canvas) {
|
||||||
|
rocks := make([]image.Rectangle, element.entity.CountChildren())
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
rocks[index] = element.entity.Child(index).Entity().Bounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||||
|
for _, tile := range tiles {
|
||||||
|
element.entity.DrawBackground(canvas.Cut(destination, tile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Box) Layout () {
|
||||||
|
margin := element.theme.Margin(tomo.PatternBackground)
|
||||||
|
padding := element.theme.Padding(tomo.PatternBackground)
|
||||||
|
bounds := element.entity.Bounds()
|
||||||
|
if element.padding { bounds = padding.Apply(bounds) }
|
||||||
|
|
||||||
|
var marginSize float64; if element.vertical {
|
||||||
|
marginSize = float64(margin.Y)
|
||||||
|
} else {
|
||||||
|
marginSize = float64(margin.X)
|
||||||
|
}
|
||||||
|
|
||||||
|
freeSpace, nExpanding := element.freeSpace()
|
||||||
|
expandingElementSize := freeSpace / nExpanding
|
||||||
|
|
||||||
|
// set the size and position of each element
|
||||||
|
x := float64(bounds.Min.X)
|
||||||
|
y := float64(bounds.Min.Y)
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
entry := element.scratch[element.entity.Child(index)]
|
||||||
|
|
||||||
|
var size float64; if entry.expand {
|
||||||
|
size = expandingElementSize
|
||||||
|
} else {
|
||||||
|
size = entry.minSize
|
||||||
|
}
|
||||||
|
|
||||||
|
var childBounds image.Rectangle; if element.vertical {
|
||||||
|
childBounds = tomo.Bounds(int(x), int(y), bounds.Dx(), int(size))
|
||||||
|
} else {
|
||||||
|
childBounds = tomo.Bounds(int(x), int(y), int(size), bounds.Dy())
|
||||||
|
}
|
||||||
|
element.entity.PlaceChild(index, childBounds)
|
||||||
|
|
||||||
|
if element.vertical {
|
||||||
|
y += size
|
||||||
|
if element.margin { y += marginSize }
|
||||||
|
} else {
|
||||||
|
x += size
|
||||||
|
if element.margin { x += marginSize }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Box) AdoptExpand (children ...tomo.Element) {
|
||||||
|
element.adopt(true, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Box) DrawBackground (destination canvas.Canvas) {
|
||||||
|
element.entity.DrawBackground(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Box) SetTheme (theme tomo.Theme) {
|
||||||
|
if theme == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = theme
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Box) freeSpace () (space float64, nExpanding float64) {
|
||||||
|
margin := element.theme.Margin(tomo.PatternBackground)
|
||||||
|
padding := element.theme.Padding(tomo.PatternBackground)
|
||||||
|
|
||||||
|
var marginSize int; if element.vertical {
|
||||||
|
marginSize = margin.Y
|
||||||
|
} else {
|
||||||
|
marginSize = margin.X
|
||||||
|
}
|
||||||
|
|
||||||
|
if element.vertical {
|
||||||
|
space = float64(element.entity.Bounds().Dy())
|
||||||
|
} else {
|
||||||
|
space = float64(element.entity.Bounds().Dx())
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, entry := range element.scratch {
|
||||||
|
if entry.expand {
|
||||||
|
nExpanding ++;
|
||||||
|
} else {
|
||||||
|
space -= float64(entry.minSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if element.padding {
|
||||||
|
space -= float64(padding.Vertical())
|
||||||
|
}
|
||||||
|
if element.margin {
|
||||||
|
space -= float64(marginSize * (len(element.scratch) - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Box) updateMinimumSize () {
|
||||||
|
margin := element.theme.Margin(tomo.PatternBackground)
|
||||||
|
padding := element.theme.Padding(tomo.PatternBackground)
|
||||||
|
var breadth, size int
|
||||||
|
var marginSize int; if element.vertical {
|
||||||
|
marginSize = margin.Y
|
||||||
|
} else {
|
||||||
|
marginSize = margin.X
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
childWidth, childHeight := element.entity.ChildMinimumSize(index)
|
||||||
|
var childBreadth, childSize int; if element.vertical {
|
||||||
|
childBreadth, childSize = childWidth, childHeight
|
||||||
|
} else {
|
||||||
|
childBreadth, childSize = childHeight, childWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
key := element.entity.Child(index)
|
||||||
|
entry := element.scratch[key]
|
||||||
|
entry.minSize = float64(childSize)
|
||||||
|
element.scratch[key] = entry
|
||||||
|
|
||||||
|
if childBreadth > breadth {
|
||||||
|
breadth = childBreadth
|
||||||
|
}
|
||||||
|
size += childSize
|
||||||
|
if element.margin && index > 0 {
|
||||||
|
size += marginSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var width, height int; if element.vertical {
|
||||||
|
width, height = breadth, size
|
||||||
|
} else {
|
||||||
|
width, height = size, breadth
|
||||||
|
}
|
||||||
|
|
||||||
|
if element.padding {
|
||||||
|
width += padding.Horizontal()
|
||||||
|
height += padding.Vertical()
|
||||||
|
}
|
||||||
|
|
||||||
|
element.entity.SetMinimumSize(width, height)
|
||||||
|
}
|
||||||
@@ -1,24 +1,19 @@
|
|||||||
package elements
|
package elements
|
||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
// import "runtime/debug"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
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/textdraw"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
||||||
|
|
||||||
// Button is a clickable button.
|
// Button is a clickable button.
|
||||||
type Button struct {
|
type Button struct {
|
||||||
*core.Core
|
entity tomo.FocusableEntity
|
||||||
*core.FocusableCore
|
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
drawer textdraw.Drawer
|
drawer textdraw.Drawer
|
||||||
|
|
||||||
|
enabled bool
|
||||||
pressed bool
|
pressed bool
|
||||||
text string
|
text string
|
||||||
|
|
||||||
@@ -34,11 +29,9 @@ type Button struct {
|
|||||||
|
|
||||||
// NewButton creates a new button with the specified label text.
|
// NewButton creates a new button with the specified label text.
|
||||||
func NewButton (text string) (element *Button) {
|
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.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 (
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
tomo.FontStyleRegular,
|
tomo.FontStyleRegular,
|
||||||
tomo.FontSizeNormal))
|
tomo.FontSizeNormal))
|
||||||
@@ -46,163 +39,19 @@ func NewButton (text string) (element *Button) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Button) HandleMouseDown (x, y int, button input.Button) {
|
// Entity returns this element's entity.
|
||||||
if !element.Enabled() { return }
|
func (element *Button) Entity () tomo.Entity {
|
||||||
if !element.Focused() { element.Focus() }
|
return element.entity
|
||||||
if button != input.ButtonLeft { return }
|
|
||||||
element.pressed = true
|
|
||||||
element.drawAndPush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Button) HandleMouseUp (x, y int, button input.Button) {
|
// Draw causes the element to draw to the specified destination canvas.
|
||||||
if button != input.ButtonLeft { return }
|
func (element *Button) Draw (destination canvas.Canvas) {
|
||||||
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 {
|
|
||||||
state := element.state()
|
state := element.state()
|
||||||
bounds := element.Bounds()
|
bounds := element.entity.Bounds()
|
||||||
pattern := element.theme.Pattern(tomo.PatternButton, state)
|
pattern := element.theme.Pattern(tomo.PatternButton, state)
|
||||||
|
|
||||||
pattern.Draw(element.core, bounds)
|
pattern.Draw(destination, bounds)
|
||||||
return []image.Rectangle { bounds }
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *Button) drawText () {
|
|
||||||
state := element.state()
|
|
||||||
bounds := element.Bounds()
|
|
||||||
foreground := element.theme.Color(tomo.ColorForeground, state)
|
foreground := element.theme.Color(tomo.ColorForeground, state)
|
||||||
sink := element.theme.Sink(tomo.PatternButton)
|
sink := element.theme.Sink(tomo.PatternButton)
|
||||||
margin := element.theme.Margin(tomo.PatternButton)
|
margin := element.theme.Margin(tomo.PatternButton)
|
||||||
@@ -240,7 +89,7 @@ func (element *Button) drawText () {
|
|||||||
}
|
}
|
||||||
offset.X += addedWidth / 2
|
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 {
|
if element.pressed {
|
||||||
offset = offset.Add(sink)
|
offset = offset.Add(sink)
|
||||||
}
|
}
|
||||||
element.drawer.Draw(element.core, foreground, offset)
|
element.drawer.Draw(destination, foreground, offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnClick sets the function to be called when the button is clicked.
|
||||||
|
func (element *Button) OnClick (callback func ()) {
|
||||||
|
element.onClick = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus gives this element input focus.
|
||||||
|
func (element *Button) Focus () {
|
||||||
|
if !element.entity.Focused() { element.entity.Focus() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled returns whether this button is enabled or not.
|
||||||
|
func (element *Button) Enabled () bool {
|
||||||
|
return element.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled sets whether this button can be clicked or not.
|
||||||
|
func (element *Button) SetEnabled (enabled bool) {
|
||||||
|
if element.enabled == enabled { return }
|
||||||
|
element.enabled = enabled
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetText sets the button's label text.
|
||||||
|
func (element *Button) SetText (text string) {
|
||||||
|
if element.text == text { return }
|
||||||
|
element.text = text
|
||||||
|
element.drawer.SetText([]rune(text))
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetIcon sets the icon of the button. Passing theme.IconNone removes the
|
||||||
|
// current icon if it exists.
|
||||||
|
func (element *Button) SetIcon (id tomo.Icon) {
|
||||||
|
if id == tomo.IconNone {
|
||||||
|
element.hasIcon = false
|
||||||
|
} else {
|
||||||
|
if element.hasIcon && element.iconId == id { return }
|
||||||
|
element.hasIcon = true
|
||||||
|
element.iconId = id
|
||||||
|
}
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShowText sets whether or not the button's text will be displayed.
|
||||||
|
func (element *Button) ShowText (showText bool) {
|
||||||
|
if element.showText == showText { return }
|
||||||
|
element.showText = showText
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Button) SetTheme (new tomo.Theme) {
|
||||||
|
if new == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = new
|
||||||
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
|
tomo.FontStyleRegular,
|
||||||
|
tomo.FontSizeNormal))
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetConfig sets the element's configuration.
|
||||||
|
func (element *Button) SetConfig (new tomo.Config) {
|
||||||
|
if new == element.config.Config { return }
|
||||||
|
element.config.Config = new
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) HandleFocusChange () {
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) HandleMouseDown (x, y int, button input.Button) {
|
||||||
|
if !element.Enabled() { return }
|
||||||
|
element.Focus()
|
||||||
|
if button != input.ButtonLeft { return }
|
||||||
|
element.pressed = true
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) HandleMouseUp (x, y int, button input.Button) {
|
||||||
|
if button != input.ButtonLeft { return }
|
||||||
|
element.pressed = false
|
||||||
|
within := image.Point { x, y }.In(element.entity.Bounds())
|
||||||
|
if element.Enabled() && within && element.onClick != nil {
|
||||||
|
element.onClick()
|
||||||
|
}
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||||
|
if !element.Enabled() { return }
|
||||||
|
if key == input.KeyEnter {
|
||||||
|
element.pressed = true
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||||
|
if key == input.KeyEnter && element.pressed {
|
||||||
|
element.pressed = false
|
||||||
|
element.entity.Invalidate()
|
||||||
|
if !element.Enabled() { return }
|
||||||
|
if element.onClick != nil {
|
||||||
|
element.onClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) updateMinimumSize () {
|
||||||
|
padding := element.theme.Padding(tomo.PatternButton)
|
||||||
|
margin := element.theme.Margin(tomo.PatternButton)
|
||||||
|
|
||||||
|
textBounds := element.drawer.LayoutBounds()
|
||||||
|
minimumSize := textBounds.Sub(textBounds.Min)
|
||||||
|
|
||||||
|
if element.hasIcon {
|
||||||
|
icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall)
|
||||||
|
if icon != nil {
|
||||||
|
bounds := icon.Bounds()
|
||||||
|
if element.showText {
|
||||||
|
minimumSize.Max.X += bounds.Dx()
|
||||||
|
minimumSize.Max.X += margin.X
|
||||||
|
} else {
|
||||||
|
minimumSize.Max.X = bounds.Dx()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
minimumSize = padding.Inverse().Apply(minimumSize)
|
||||||
|
element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Button) state () tomo.State {
|
||||||
|
return tomo.State {
|
||||||
|
Disabled: !element.Enabled(),
|
||||||
|
Focused: element.entity.Focused(),
|
||||||
|
Pressed: element.pressed,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
165
elements/cell.go
Normal file
165
elements/cell.go
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
|
|
||||||
|
type cellEntity interface {
|
||||||
|
tomo.ContainerEntity
|
||||||
|
tomo.SelectableEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cell is a single-element container that satisfies tomo.Selectable. It
|
||||||
|
// provides styling based on whether or not it is selected.
|
||||||
|
type Cell struct {
|
||||||
|
entity cellEntity
|
||||||
|
child tomo.Element
|
||||||
|
enabled bool
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
|
onSelectionChange func ()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewCell creates a new cell element. If padding is true, the cell will have
|
||||||
|
// padding on all sides. Child can be nil and added later with the Adopt()
|
||||||
|
// method.
|
||||||
|
func NewCell (child tomo.Element) (element *Cell) {
|
||||||
|
element = &Cell { enabled: true }
|
||||||
|
element.theme.Case = tomo.C("tomo", "cell")
|
||||||
|
element.entity = tomo.NewEntity(element).(cellEntity)
|
||||||
|
element.Adopt(child)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Entity returns this element's entity.
|
||||||
|
func (element *Cell) Entity () tomo.Entity {
|
||||||
|
return element.entity
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw causes the element to draw to the specified destination canvas.
|
||||||
|
func (element *Cell) Draw (destination canvas.Canvas) {
|
||||||
|
bounds := element.entity.Bounds()
|
||||||
|
pattern := element.theme.Pattern(tomo.PatternTableCell, element.state())
|
||||||
|
if element.child == nil {
|
||||||
|
pattern.Draw(destination, bounds)
|
||||||
|
} else {
|
||||||
|
artist.DrawShatter (
|
||||||
|
destination, pattern, bounds,
|
||||||
|
element.child.Entity().Bounds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw causes the element to perform a layout operation.
|
||||||
|
func (element *Cell) Layout () {
|
||||||
|
if element.child == nil { return }
|
||||||
|
|
||||||
|
bounds := element.entity.Bounds()
|
||||||
|
bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds)
|
||||||
|
|
||||||
|
element.entity.PlaceChild(0, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawBackground draws this element's background pattern to the specified
|
||||||
|
// destination canvas.
|
||||||
|
func (element *Cell) DrawBackground (destination canvas.Canvas) {
|
||||||
|
element.theme.Pattern(tomo.PatternTableCell, element.state()).
|
||||||
|
Draw(destination, element.entity.Bounds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Adopt sets this element's child. If nil is passed, any child is removed.
|
||||||
|
func (element *Cell) Adopt (child tomo.Element) {
|
||||||
|
if element.child != nil {
|
||||||
|
element.entity.Disown(element.entity.IndexOf(element.child))
|
||||||
|
}
|
||||||
|
if child != nil {
|
||||||
|
element.entity.Adopt(child)
|
||||||
|
}
|
||||||
|
element.child = child
|
||||||
|
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.invalidateChild()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Child returns this element's child. If there is no child, this method will
|
||||||
|
// return nil.
|
||||||
|
func (element *Cell) Child () tomo.Element {
|
||||||
|
return element.child
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enabled returns whether this cell is enabled or not.
|
||||||
|
func (element *Cell) Enabled () bool {
|
||||||
|
return element.enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetEnabled sets whether this cell can be selected or not.
|
||||||
|
func (element *Cell) SetEnabled (enabled bool) {
|
||||||
|
if element.enabled == enabled { return }
|
||||||
|
element.enabled = enabled
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.invalidateChild()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets this element's theme.
|
||||||
|
func (element *Cell) SetTheme (theme tomo.Theme) {
|
||||||
|
if theme == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = theme
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.invalidateChild()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnSelectionChange sets a function to be called when this element is selected
|
||||||
|
// or unselected.
|
||||||
|
func (element *Cell) OnSelectionChange (callback func ()) {
|
||||||
|
element.onSelectionChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Cell) Selected () bool {
|
||||||
|
return element.entity.Selected()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Cell) HandleSelectionChange () {
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.invalidateChild()
|
||||||
|
if element.onSelectionChange != nil {
|
||||||
|
element.onSelectionChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) {
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Cell) state () tomo.State {
|
||||||
|
return tomo.State {
|
||||||
|
Disabled: !element.enabled,
|
||||||
|
On: element.entity.Selected(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Cell) updateMinimumSize () {
|
||||||
|
width, height := 0, 0
|
||||||
|
|
||||||
|
if element.child != nil {
|
||||||
|
childWidth, childHeight := element.entity.ChildMinimumSize(0)
|
||||||
|
width += childWidth
|
||||||
|
height += childHeight
|
||||||
|
}
|
||||||
|
padding := element.theme.Padding(tomo.PatternTableCell)
|
||||||
|
width += padding.Horizontal()
|
||||||
|
height += padding.Vertical()
|
||||||
|
|
||||||
|
element.entity.SetMinimumSize(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Cell) invalidateChild () {
|
||||||
|
if element.child != nil {
|
||||||
|
element.child.Entity().Invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,19 +3,17 @@ package elements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
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/textdraw"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
// Checkbox is a toggle-able checkbox with a label.
|
// Checkbox is a toggle-able checkbox with a label.
|
||||||
type Checkbox struct {
|
type Checkbox struct {
|
||||||
*core.Core
|
entity tomo.FocusableEntity
|
||||||
*core.FocusableCore
|
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
drawer textdraw.Drawer
|
drawer textdraw.Drawer
|
||||||
|
|
||||||
|
enabled bool
|
||||||
pressed bool
|
pressed bool
|
||||||
checked bool
|
checked bool
|
||||||
text string
|
text string
|
||||||
@@ -28,11 +26,9 @@ type Checkbox struct {
|
|||||||
|
|
||||||
// NewCheckbox creates a new cbeckbox with the specified label text.
|
// NewCheckbox creates a new cbeckbox with the specified label text.
|
||||||
func NewCheckbox (text string, checked bool) (element *Checkbox) {
|
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.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 (
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
tomo.FontStyleRegular,
|
tomo.FontStyleRegular,
|
||||||
tomo.FontSizeNormal))
|
tomo.FontSizeNormal))
|
||||||
@@ -40,57 +36,39 @@ func NewCheckbox (text string, checked bool) (element *Checkbox) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) {
|
// Entity returns this element's entity.
|
||||||
if !element.Enabled() { return }
|
func (element *Checkbox) Entity () tomo.Entity {
|
||||||
element.Focus()
|
return element.entity
|
||||||
element.pressed = true
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) {
|
// Draw causes the element to draw to the specified destination canvas.
|
||||||
if button != input.ButtonLeft || !element.pressed { return }
|
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
|
state := tomo.State {
|
||||||
within := image.Point { x, y }.
|
Disabled: !element.Enabled(),
|
||||||
In(element.Bounds())
|
Focused: element.entity.Focused(),
|
||||||
if within {
|
Pressed: element.pressed,
|
||||||
element.checked = !element.checked
|
On: element.checked,
|
||||||
}
|
}
|
||||||
|
|
||||||
if element.core.HasImage() {
|
element.entity.DrawBackground(destination)
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
if within && element.onToggle != nil {
|
|
||||||
element.onToggle()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
pattern := element.theme.Pattern(tomo.PatternButton, state)
|
||||||
if key == input.KeyEnter {
|
pattern.Draw(destination, boxBounds)
|
||||||
element.pressed = true
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) {
|
textBounds := element.drawer.LayoutBounds()
|
||||||
if key == input.KeyEnter && element.pressed {
|
margin := element.theme.Margin(tomo.PatternBackground)
|
||||||
element.pressed = false
|
offset := bounds.Min.Add(image.Point {
|
||||||
element.checked = !element.checked
|
X: bounds.Dy() + margin.X,
|
||||||
if element.core.HasImage() {
|
})
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
offset.Y -= textBounds.Min.Y
|
||||||
}
|
offset.X -= textBounds.Min.X
|
||||||
if element.onToggle != nil {
|
|
||||||
element.onToggle()
|
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.
|
// 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
|
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.
|
// SetEnabled sets whether this checkbox can be toggled or not.
|
||||||
func (element *Checkbox) SetEnabled (enabled bool) {
|
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.
|
// SetText sets the checkbox's label text.
|
||||||
func (element *Checkbox) SetText (text string) {
|
func (element *Checkbox) SetText (text string) {
|
||||||
if element.text == text { return }
|
if element.text == text { return }
|
||||||
|
|
||||||
element.text = text
|
element.text = text
|
||||||
element.drawer.SetText([]rune(text))
|
element.drawer.SetText([]rune(text))
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets the element's theme.
|
// SetTheme sets the element's theme.
|
||||||
@@ -130,7 +115,7 @@ func (element *Checkbox) SetTheme (new tomo.Theme) {
|
|||||||
tomo.FontStyleRegular,
|
tomo.FontStyleRegular,
|
||||||
tomo.FontSizeNormal))
|
tomo.FontSizeNormal))
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
@@ -138,54 +123,61 @@ func (element *Checkbox) SetConfig (new tomo.Config) {
|
|||||||
if new == element.config.Config { return }
|
if new == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.updateMinimumSize()
|
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 () {
|
func (element *Checkbox) updateMinimumSize () {
|
||||||
textBounds := element.drawer.LayoutBounds()
|
textBounds := element.drawer.LayoutBounds()
|
||||||
if element.text == "" {
|
if element.text == "" {
|
||||||
element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
|
element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy())
|
||||||
} else {
|
} else {
|
||||||
margin := element.theme.Margin(tomo.PatternBackground)
|
margin := element.theme.Margin(tomo.PatternBackground)
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
textBounds.Dy() + margin.X + textBounds.Dx(),
|
textBounds.Dy() + margin.X + textBounds.Dx(),
|
||||||
textBounds.Dy())
|
textBounds.Dy())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Checkbox) redo () {
|
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *Checkbox) draw () {
|
|
||||||
bounds := element.Bounds()
|
|
||||||
boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
|
||||||
|
|
||||||
state := tomo.State {
|
|
||||||
Disabled: !element.Enabled(),
|
|
||||||
Focused: element.Focused(),
|
|
||||||
Pressed: element.pressed,
|
|
||||||
On: element.checked,
|
|
||||||
}
|
|
||||||
|
|
||||||
element.core.DrawBackground (
|
|
||||||
element.theme.Pattern(tomo.PatternBackground, state))
|
|
||||||
|
|
||||||
pattern := element.theme.Pattern(tomo.PatternButton, state)
|
|
||||||
pattern.Draw(element.core, boxBounds)
|
|
||||||
|
|
||||||
textBounds := element.drawer.LayoutBounds()
|
|
||||||
margin := element.theme.Margin(tomo.PatternBackground)
|
|
||||||
offset := bounds.Min.Add(image.Point {
|
|
||||||
X: bounds.Dy() + margin.X,
|
|
||||||
})
|
|
||||||
|
|
||||||
offset.Y -= textBounds.Min.Y
|
|
||||||
offset.X -= textBounds.Min.X
|
|
||||||
|
|
||||||
foreground := element.theme.Color(tomo.ColorForeground, state)
|
|
||||||
element.drawer.Draw(element.core, foreground, offset)
|
|
||||||
}
|
|
||||||
|
|||||||
77
elements/container.go
Normal file
77
elements/container.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
|
||||||
|
type scratchEntry struct {
|
||||||
|
expand bool
|
||||||
|
minSize float64
|
||||||
|
minBreadth float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type container struct {
|
||||||
|
entity tomo.ContainerEntity
|
||||||
|
scratch map[tomo.Element] scratchEntry
|
||||||
|
minimumSize func ()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) Entity () tomo.Entity {
|
||||||
|
return container.entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) Adopt (children ...tomo.Element) {
|
||||||
|
container.adopt(false, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) init () {
|
||||||
|
container.scratch = make(map[tomo.Element] scratchEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) adopt (expand bool, children ...tomo.Element) {
|
||||||
|
for _, child := range children {
|
||||||
|
container.entity.Adopt(child)
|
||||||
|
container.scratch[child] = scratchEntry { expand: expand }
|
||||||
|
}
|
||||||
|
container.minimumSize()
|
||||||
|
container.entity.Invalidate()
|
||||||
|
container.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) Disown (children ...tomo.Element) {
|
||||||
|
for _, child := range children {
|
||||||
|
index := container.entity.IndexOf(child)
|
||||||
|
if index < 0 { continue }
|
||||||
|
container.entity.Disown(index)
|
||||||
|
delete(container.scratch, child)
|
||||||
|
}
|
||||||
|
container.minimumSize()
|
||||||
|
container.entity.Invalidate()
|
||||||
|
container.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) DisownAll () {
|
||||||
|
func () {
|
||||||
|
for index := 0; index < container.entity.CountChildren(); index ++ {
|
||||||
|
index := index
|
||||||
|
defer container.entity.Disown(index)
|
||||||
|
}
|
||||||
|
} ()
|
||||||
|
container.scratch = make(map[tomo.Element] scratchEntry)
|
||||||
|
container.minimumSize()
|
||||||
|
container.entity.Invalidate()
|
||||||
|
container.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) Child (index int) tomo.Element {
|
||||||
|
if index < 0 || index >= container.entity.CountChildren() { return nil }
|
||||||
|
return container.entity.Child(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) CountChildren () int {
|
||||||
|
return container.entity.CountChildren()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (container *container) HandleChildMinimumSizeChange (child tomo.Element) {
|
||||||
|
container.minimumSize()
|
||||||
|
container.entity.Invalidate()
|
||||||
|
container.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
312
elements/directory.go
Normal file
312
elements/directory.go
Normal file
@@ -0,0 +1,312 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "path/filepath"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
|
|
||||||
|
// TODO: base on flow implementation of list. also be able to switch to a table
|
||||||
|
// variant for a more information dense view.
|
||||||
|
|
||||||
|
type directoryEntity interface {
|
||||||
|
tomo.ContainerEntity
|
||||||
|
tomo.ScrollableEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
type historyEntry struct {
|
||||||
|
location string
|
||||||
|
filesystem ReadDirStatFS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Directory displays a list of files within a particular directory and
|
||||||
|
// file system.
|
||||||
|
type Directory struct {
|
||||||
|
container
|
||||||
|
entity directoryEntity
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
|
scroll image.Point
|
||||||
|
contentBounds image.Rectangle
|
||||||
|
|
||||||
|
history []historyEntry
|
||||||
|
historyIndex int
|
||||||
|
|
||||||
|
onChoose func (file string)
|
||||||
|
onScrollBoundsChange func ()
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDirectory creates a new directory view. If within is nil, it will use
|
||||||
|
// the OS file system.
|
||||||
|
func NewDirectory (
|
||||||
|
location string,
|
||||||
|
within ReadDirStatFS,
|
||||||
|
) (
|
||||||
|
element *Directory,
|
||||||
|
err error,
|
||||||
|
) {
|
||||||
|
element = &Directory { }
|
||||||
|
element.theme.Case = tomo.C("tomo", "list")
|
||||||
|
element.entity = tomo.NewEntity(element).(directoryEntity)
|
||||||
|
element.container.entity = element.entity
|
||||||
|
element.minimumSize = element.updateMinimumSize
|
||||||
|
element.init()
|
||||||
|
err = element.SetLocation(location, within)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Directory) Draw (destination canvas.Canvas) {
|
||||||
|
rocks := make([]image.Rectangle, element.entity.CountChildren())
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
rocks[index] = element.entity.Child(index).Entity().Bounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||||
|
for _, tile := range tiles {
|
||||||
|
element.DrawBackground(canvas.Cut(destination, tile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Directory) Layout () {
|
||||||
|
if element.scroll.Y > element.maxScrollHeight() {
|
||||||
|
element.scroll.Y = element.maxScrollHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
margin := element.theme.Margin(tomo.PatternPinboard)
|
||||||
|
padding := element.theme.Padding(tomo.PatternPinboard)
|
||||||
|
bounds := padding.Apply(element.entity.Bounds())
|
||||||
|
element.contentBounds = image.Rectangle { }
|
||||||
|
|
||||||
|
dot := bounds.Min.Sub(element.scroll)
|
||||||
|
xStart := dot.X
|
||||||
|
rowHeight := 0
|
||||||
|
|
||||||
|
nextLine := func () {
|
||||||
|
dot.X = xStart
|
||||||
|
dot.Y += margin.Y
|
||||||
|
dot.Y += rowHeight
|
||||||
|
rowHeight = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
child := element.entity.Child(index)
|
||||||
|
entry := element.scratch[child]
|
||||||
|
|
||||||
|
width := int(entry.minBreadth)
|
||||||
|
height := int(entry.minSize)
|
||||||
|
if width + dot.X > bounds.Max.X {
|
||||||
|
nextLine()
|
||||||
|
}
|
||||||
|
if typedChild, ok := child.(tomo.Flexible); ok {
|
||||||
|
height = typedChild.FlexibleHeightFor(width)
|
||||||
|
}
|
||||||
|
if rowHeight < height {
|
||||||
|
rowHeight = height
|
||||||
|
}
|
||||||
|
|
||||||
|
childBounds := tomo.Bounds (
|
||||||
|
dot.X, dot.Y,
|
||||||
|
width, height)
|
||||||
|
element.entity.PlaceChild(index, childBounds)
|
||||||
|
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||||
|
|
||||||
|
dot.X += width + margin.X
|
||||||
|
}
|
||||||
|
|
||||||
|
element.contentBounds =
|
||||||
|
element.contentBounds.Sub(element.contentBounds.Min)
|
||||||
|
|
||||||
|
element.entity.NotifyScrollBoundsChange()
|
||||||
|
if element.onScrollBoundsChange != nil {
|
||||||
|
element.onScrollBoundsChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Directory) HandleMouseDown (x, y int, button input.Button) {
|
||||||
|
element.selectNone()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Directory) HandleMouseUp (x, y int, button input.Button) { }
|
||||||
|
|
||||||
|
func (element *Directory) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) {
|
||||||
|
element.selectNone()
|
||||||
|
if child, ok := child.(tomo.Selectable); ok {
|
||||||
|
index := element.entity.IndexOf(child)
|
||||||
|
element.entity.SelectChild(index, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Directory) HandleChildMouseUp (int, int, input.Button, tomo.Element) { }
|
||||||
|
|
||||||
|
func (element *Directory) HandleChildFlexibleHeightChange (child tomo.Flexible) {
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollContentBounds returns the full content size of the element.
|
||||||
|
func (element *Directory) ScrollContentBounds () image.Rectangle {
|
||||||
|
return element.contentBounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollViewportBounds returns the size and position of the element's
|
||||||
|
// viewport relative to ScrollBounds.
|
||||||
|
func (element *Directory) ScrollViewportBounds () image.Rectangle {
|
||||||
|
padding := element.theme.Padding(tomo.PatternPinboard)
|
||||||
|
bounds := padding.Apply(element.entity.Bounds())
|
||||||
|
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollTo scrolls the viewport to the specified point relative to
|
||||||
|
// ScrollBounds.
|
||||||
|
func (element *Directory) ScrollTo (position image.Point) {
|
||||||
|
if position.Y < 0 {
|
||||||
|
position.Y = 0
|
||||||
|
}
|
||||||
|
maxScrollHeight := element.maxScrollHeight()
|
||||||
|
if position.Y > maxScrollHeight {
|
||||||
|
position.Y = maxScrollHeight
|
||||||
|
}
|
||||||
|
element.scroll = position
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||||
|
// bounds, content bounds, or scroll axes change.
|
||||||
|
func (element *Directory) OnScrollBoundsChange (callback func ()) {
|
||||||
|
element.onScrollBoundsChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollAxes returns the supported axes for scrolling.
|
||||||
|
func (element *Directory) ScrollAxes () (horizontal, vertical bool) {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Directory) DrawBackground (destination canvas.Canvas) {
|
||||||
|
element.theme.Pattern(tomo.PatternPinboard, tomo.State { }).
|
||||||
|
Draw(destination, element.entity.Bounds())
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Directory) SetTheme (theme tomo.Theme) {
|
||||||
|
if theme == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = theme
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Location returns the directory's location and filesystem.
|
||||||
|
func (element *Directory) Location () (string, ReadDirStatFS) {
|
||||||
|
if len(element.history) < 1 { return "", nil }
|
||||||
|
current := element.history[element.historyIndex]
|
||||||
|
return current.location, current.filesystem
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetLocation sets the directory's location and filesystem. If within is nil,
|
||||||
|
// it will use the OS file system.
|
||||||
|
func (element *Directory) SetLocation (
|
||||||
|
location string,
|
||||||
|
within ReadDirStatFS,
|
||||||
|
) error {
|
||||||
|
if within == nil {
|
||||||
|
within = defaultFS { }
|
||||||
|
}
|
||||||
|
element.scroll = image.Point { }
|
||||||
|
|
||||||
|
if element.history != nil {
|
||||||
|
element.historyIndex ++
|
||||||
|
}
|
||||||
|
element.history = append (
|
||||||
|
element.history[:element.historyIndex],
|
||||||
|
historyEntry { location, within })
|
||||||
|
return element.Update()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backward goes back a directory in history
|
||||||
|
func (element *Directory) Backward () (bool, error) {
|
||||||
|
if element.historyIndex > 1 {
|
||||||
|
element.historyIndex --
|
||||||
|
return true, element.Update()
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Forward goes forward a directory in history
|
||||||
|
func (element *Directory) Forward () (bool, error) {
|
||||||
|
if element.historyIndex < len(element.history) - 1 {
|
||||||
|
element.historyIndex ++
|
||||||
|
return true, element.Update()
|
||||||
|
} else {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update refreshes the directory's contents.
|
||||||
|
func (element *Directory) Update () error {
|
||||||
|
location, filesystem := element.Location()
|
||||||
|
entries, err := filesystem.ReadDir(location)
|
||||||
|
|
||||||
|
children := make([]tomo.Element, len(entries))
|
||||||
|
for index, entry := range entries {
|
||||||
|
filePath := filepath.Join(location, entry.Name())
|
||||||
|
file, _ := NewFile(filePath, filesystem)
|
||||||
|
file.OnChoose (func () {
|
||||||
|
if element.onChoose != nil {
|
||||||
|
element.onChoose(filePath)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
children[index] = file
|
||||||
|
}
|
||||||
|
|
||||||
|
element.DisownAll()
|
||||||
|
element.Adopt(children...)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnChoose sets a function to be called when the user double-clicks a file or
|
||||||
|
// sub-directory within the directory view.
|
||||||
|
func (element *Directory) OnChoose (callback func (file string)) {
|
||||||
|
element.onChoose = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Directory) selectNone () {
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
element.entity.SelectChild(index, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Directory) maxScrollHeight () (height int) {
|
||||||
|
padding := element.theme.Padding(tomo.PatternSunken)
|
||||||
|
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
|
||||||
|
height = element.contentBounds.Dy() - viewportHeight
|
||||||
|
if height < 0 { height = 0 }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func (element *Directory) updateMinimumSize () {
|
||||||
|
padding := element.theme.Padding(tomo.PatternPinboard)
|
||||||
|
minimumWidth := 0
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
width, height := element.entity.ChildMinimumSize(index)
|
||||||
|
if width > minimumWidth {
|
||||||
|
minimumWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
|
key := element.entity.Child(index)
|
||||||
|
entry := element.scratch[key]
|
||||||
|
entry.minSize = float64(height)
|
||||||
|
entry.minBreadth = float64(width)
|
||||||
|
element.scratch[key] = entry
|
||||||
|
}
|
||||||
|
element.entity.SetMinimumSize (
|
||||||
|
minimumWidth + padding.Horizontal(),
|
||||||
|
padding.Vertical())
|
||||||
|
}
|
||||||
208
elements/document.go
Normal file
208
elements/document.go
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
|
|
||||||
|
type documentEntity interface {
|
||||||
|
tomo.ContainerEntity
|
||||||
|
tomo.ScrollableEntity
|
||||||
|
}
|
||||||
|
|
||||||
|
type Document struct {
|
||||||
|
container
|
||||||
|
entity documentEntity
|
||||||
|
|
||||||
|
scroll image.Point
|
||||||
|
contentBounds image.Rectangle
|
||||||
|
|
||||||
|
theme theme.Wrapped
|
||||||
|
|
||||||
|
onScrollBoundsChange func ()
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDocument (children ...tomo.Element) (element *Document) {
|
||||||
|
element = &Document { }
|
||||||
|
element.theme.Case = tomo.C("tomo", "document")
|
||||||
|
element.entity = tomo.NewEntity(element).(documentEntity)
|
||||||
|
element.container.entity = element.entity
|
||||||
|
element.minimumSize = element.updateMinimumSize
|
||||||
|
element.init()
|
||||||
|
element.Adopt(children...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Document) Draw (destination canvas.Canvas) {
|
||||||
|
rocks := make([]image.Rectangle, element.entity.CountChildren())
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
rocks[index] = element.entity.Child(index).Entity().Bounds()
|
||||||
|
}
|
||||||
|
|
||||||
|
tiles := shatter.Shatter(element.entity.Bounds(), rocks...)
|
||||||
|
for _, tile := range tiles {
|
||||||
|
element.entity.DrawBackground(canvas.Cut(destination, tile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Document) Layout () {
|
||||||
|
if element.scroll.Y > element.maxScrollHeight() {
|
||||||
|
element.scroll.Y = element.maxScrollHeight()
|
||||||
|
}
|
||||||
|
|
||||||
|
margin := element.theme.Margin(tomo.PatternBackground)
|
||||||
|
padding := element.theme.Padding(tomo.PatternBackground)
|
||||||
|
bounds := padding.Apply(element.entity.Bounds())
|
||||||
|
element.contentBounds = image.Rectangle { }
|
||||||
|
|
||||||
|
dot := bounds.Min.Sub(element.scroll)
|
||||||
|
xStart := dot.X
|
||||||
|
rowHeight := 0
|
||||||
|
|
||||||
|
nextLine := func () {
|
||||||
|
dot.X = xStart
|
||||||
|
dot.Y += margin.Y
|
||||||
|
dot.Y += rowHeight
|
||||||
|
rowHeight = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
child := element.entity.Child(index)
|
||||||
|
entry := element.scratch[child]
|
||||||
|
|
||||||
|
if dot.X > xStart && entry.expand {
|
||||||
|
nextLine()
|
||||||
|
}
|
||||||
|
|
||||||
|
width := int(entry.minBreadth)
|
||||||
|
height := int(entry.minSize)
|
||||||
|
if width + dot.X > bounds.Max.X && !entry.expand {
|
||||||
|
nextLine()
|
||||||
|
}
|
||||||
|
if width < bounds.Dx() && entry.expand {
|
||||||
|
width = bounds.Dx()
|
||||||
|
}
|
||||||
|
if typedChild, ok := child.(tomo.Flexible); ok {
|
||||||
|
height = typedChild.FlexibleHeightFor(width)
|
||||||
|
}
|
||||||
|
if rowHeight < height {
|
||||||
|
rowHeight = height
|
||||||
|
}
|
||||||
|
|
||||||
|
childBounds := tomo.Bounds (
|
||||||
|
dot.X, dot.Y,
|
||||||
|
width, height)
|
||||||
|
element.entity.PlaceChild(index, childBounds)
|
||||||
|
element.contentBounds = element.contentBounds.Union(childBounds)
|
||||||
|
|
||||||
|
if entry.expand {
|
||||||
|
nextLine()
|
||||||
|
} else {
|
||||||
|
dot.X += width + margin.X
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
element.contentBounds =
|
||||||
|
element.contentBounds.Sub(element.contentBounds.Min)
|
||||||
|
|
||||||
|
element.entity.NotifyScrollBoundsChange()
|
||||||
|
if element.onScrollBoundsChange != nil {
|
||||||
|
element.onScrollBoundsChange()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Document) Adopt (children ...tomo.Element) {
|
||||||
|
element.adopt(true, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Document) AdoptInline (children ...tomo.Element) {
|
||||||
|
element.adopt(false, children...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) {
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Document) DrawBackground (destination canvas.Canvas) {
|
||||||
|
element.entity.DrawBackground(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTheme sets the element's theme.
|
||||||
|
func (element *Document) SetTheme (theme tomo.Theme) {
|
||||||
|
if theme == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = theme
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollContentBounds returns the full content size of the element.
|
||||||
|
func (element *Document) ScrollContentBounds () image.Rectangle {
|
||||||
|
return element.contentBounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollViewportBounds returns the size and position of the element's
|
||||||
|
// viewport relative to ScrollBounds.
|
||||||
|
func (element *Document) ScrollViewportBounds () image.Rectangle {
|
||||||
|
padding := element.theme.Padding(tomo.PatternBackground)
|
||||||
|
bounds := padding.Apply(element.entity.Bounds())
|
||||||
|
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
||||||
|
return bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollTo scrolls the viewport to the specified point relative to
|
||||||
|
// ScrollBounds.
|
||||||
|
func (element *Document) ScrollTo (position image.Point) {
|
||||||
|
if position.Y < 0 {
|
||||||
|
position.Y = 0
|
||||||
|
}
|
||||||
|
maxScrollHeight := element.maxScrollHeight()
|
||||||
|
if position.Y > maxScrollHeight {
|
||||||
|
position.Y = maxScrollHeight
|
||||||
|
}
|
||||||
|
element.scroll = position
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
||||||
|
// bounds, content bounds, or scroll axes change.
|
||||||
|
func (element *Document) OnScrollBoundsChange (callback func ()) {
|
||||||
|
element.onScrollBoundsChange = callback
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollAxes returns the supported axes for scrolling.
|
||||||
|
func (element *Document) ScrollAxes () (horizontal, vertical bool) {
|
||||||
|
return false, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Document) maxScrollHeight () (height int) {
|
||||||
|
padding := element.theme.Padding(tomo.PatternSunken)
|
||||||
|
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
|
||||||
|
height = element.contentBounds.Dy() - viewportHeight
|
||||||
|
if height < 0 { height = 0 }
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Document) updateMinimumSize () {
|
||||||
|
padding := element.theme.Padding(tomo.PatternBackground)
|
||||||
|
minimumWidth := 0
|
||||||
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
width, height := element.entity.ChildMinimumSize(index)
|
||||||
|
if width > minimumWidth {
|
||||||
|
minimumWidth = width
|
||||||
|
}
|
||||||
|
|
||||||
|
key := element.entity.Child(index)
|
||||||
|
entry := element.scratch[key]
|
||||||
|
entry.minSize = float64(height)
|
||||||
|
entry.minBreadth = float64(width)
|
||||||
|
element.scratch[key] = entry
|
||||||
|
}
|
||||||
|
element.entity.SetMinimumSize (
|
||||||
|
minimumWidth + padding.Horizontal(),
|
||||||
|
padding.Vertical())
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package fileElements
|
package elements
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
import "io/fs"
|
import "io/fs"
|
||||||
@@ -6,27 +6,29 @@ import "image"
|
|||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
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 displays an interactive visual representation of a file within any
|
||||||
// file system.
|
// file system.
|
||||||
type File struct {
|
type File struct {
|
||||||
*core.Core
|
entity fileEntity
|
||||||
*core.FocusableCore
|
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
|
|
||||||
config config.Wrapped
|
config config.Wrapped
|
||||||
theme theme.Wrapped
|
theme theme.Wrapped
|
||||||
|
|
||||||
lastClick time.Time
|
lastClick time.Time
|
||||||
pressed bool
|
pressed bool
|
||||||
|
enabled bool
|
||||||
iconID tomo.Icon
|
iconID tomo.Icon
|
||||||
filesystem fs.StatFS
|
filesystem fs.StatFS
|
||||||
location string
|
location string
|
||||||
selected bool
|
|
||||||
|
|
||||||
onChoose func ()
|
onChoose func ()
|
||||||
}
|
}
|
||||||
@@ -40,15 +42,44 @@ func NewFile (
|
|||||||
element *File,
|
element *File,
|
||||||
err error,
|
err error,
|
||||||
) {
|
) {
|
||||||
element = &File { }
|
element = &File { enabled: true }
|
||||||
element.theme.Case = tomo.C("files", "file")
|
element.theme.Case = tomo.C("files", "file")
|
||||||
element.Core, element.core = core.NewCore(element, element.drawAll)
|
element.entity = tomo.NewEntity(element).(fileEntity)
|
||||||
element.FocusableCore,
|
|
||||||
element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush)
|
|
||||||
err = element.SetLocation(location, within)
|
err = element.SetLocation(location, within)
|
||||||
return
|
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.
|
// Location returns the file's location and filesystem.
|
||||||
func (element *File) Location () (string, fs.StatFS) {
|
func (element *File) Location () (string, fs.StatFS) {
|
||||||
return element.location, element.filesystem
|
return element.location, element.filesystem
|
||||||
@@ -82,55 +113,70 @@ func (element *File) Update () error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
return err
|
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) {
|
func (element *File) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||||
if !element.Enabled() { return }
|
if !element.Enabled() { return }
|
||||||
if key == input.KeyEnter {
|
if key == input.KeyEnter {
|
||||||
element.pressed = true
|
element.pressed = true
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *File) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
func (element *File) HandleKeyUp(key input.Key, modifiers input.Modifiers) {
|
||||||
if key == input.KeyEnter && element.pressed {
|
if key == input.KeyEnter && element.pressed {
|
||||||
element.pressed = false
|
element.pressed = false
|
||||||
element.drawAndPush()
|
|
||||||
if !element.Enabled() { return }
|
if !element.Enabled() { return }
|
||||||
|
element.entity.Invalidate()
|
||||||
if element.onChoose != nil {
|
if element.onChoose != nil {
|
||||||
element.onChoose()
|
element.onChoose()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (element *File) HandleFocusChange () {
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *File) HandleSelectionChange () {
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
func (element *File) OnChoose (callback func ()) {
|
func (element *File) OnChoose (callback func ()) {
|
||||||
element.onChoose = callback
|
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) {
|
func (element *File) HandleMouseDown (x, y int, button input.Button) {
|
||||||
if !element.Enabled() { return }
|
if !element.Enabled() { return }
|
||||||
if !element.Focused() { element.Focus() }
|
if !element.entity.Focused() { element.Focus() }
|
||||||
if button != input.ButtonLeft { return }
|
if button != input.ButtonLeft { return }
|
||||||
element.pressed = true
|
element.pressed = true
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *File) HandleMouseUp (x, y int, button input.Button) {
|
func (element *File) HandleMouseUp (x, y int, button input.Button) {
|
||||||
if button != input.ButtonLeft { return }
|
if button != input.ButtonLeft { return }
|
||||||
element.pressed = false
|
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 time.Since(element.lastClick) < element.config.DoubleClickDelay() {
|
||||||
if element.Enabled() && within && element.onChoose != nil {
|
if element.Enabled() && within && element.onChoose != nil {
|
||||||
element.onChoose()
|
element.onChoose()
|
||||||
@@ -138,29 +184,29 @@ func (element *File) HandleMouseUp (x, y int, button input.Button) {
|
|||||||
} else {
|
} else {
|
||||||
element.lastClick = time.Now()
|
element.lastClick = time.Now()
|
||||||
}
|
}
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets the element's theme.
|
// SetTheme sets the element's theme.
|
||||||
func (element *File) SetTheme (new tomo.Theme) {
|
func (element *File) SetTheme (theme tomo.Theme) {
|
||||||
if new == element.theme.Theme { return }
|
if theme == element.theme.Theme { return }
|
||||||
element.theme.Theme = new
|
element.theme.Theme = theme
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
func (element *File) SetConfig (new tomo.Config) {
|
func (element *File) SetConfig (config tomo.Config) {
|
||||||
if new == element.config.Config { return }
|
if config == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = config
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *File) state () tomo.State {
|
func (element *File) state () tomo.State {
|
||||||
return tomo.State {
|
return tomo.State {
|
||||||
Disabled: !element.Enabled(),
|
Disabled: !element.Enabled(),
|
||||||
Focused: element.Focused(),
|
Focused: element.entity.Focused(),
|
||||||
Pressed: element.pressed,
|
Pressed: element.pressed,
|
||||||
On: element.selected,
|
On: element.entity.Selected(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,44 +218,11 @@ func (element *File) updateMinimumSize () {
|
|||||||
padding := element.theme.Padding(tomo.PatternButton)
|
padding := element.theme.Padding(tomo.PatternButton)
|
||||||
icon := element.icon()
|
icon := element.icon()
|
||||||
if icon == nil {
|
if icon == nil {
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
padding.Horizontal(),
|
padding.Horizontal(),
|
||||||
padding.Vertical())
|
padding.Vertical())
|
||||||
} else {
|
} else {
|
||||||
bounds := padding.Inverse().Apply(icon.Bounds())
|
bounds := padding.Inverse().Apply(icon.Bounds())
|
||||||
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
element.entity.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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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 "os"
|
||||||
import "io/fs"
|
import "io/fs"
|
||||||
@@ -5,18 +5,14 @@ import "math"
|
|||||||
import "image"
|
import "image"
|
||||||
import "image/color"
|
import "image/color"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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/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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
|
||||||
|
|
||||||
// AnalogClock can display the time of day in an analog format.
|
// AnalogClock can display the time of day in an analog format.
|
||||||
type AnalogClock struct {
|
type AnalogClock struct {
|
||||||
*core.Core
|
entity tomo.Entity
|
||||||
core core.CoreControl
|
time time.Time
|
||||||
time time.Time
|
|
||||||
|
|
||||||
config config.Wrapped
|
|
||||||
theme theme.Wrapped
|
theme theme.Wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,46 +20,24 @@ type AnalogClock struct {
|
|||||||
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
|
func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
|
||||||
element = &AnalogClock { }
|
element = &AnalogClock { }
|
||||||
element.theme.Case = tomo.C("tomo", "clock")
|
element.theme.Case = tomo.C("tomo", "clock")
|
||||||
element.Core, element.core = core.NewCore(element, element.draw)
|
element.entity = tomo.NewEntity(element)
|
||||||
element.core.SetMinimumSize(64, 64)
|
element.entity.SetMinimumSize(64, 64)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTime changes the time that the clock displays.
|
// Entity returns this element's entity.
|
||||||
func (element *AnalogClock) SetTime (newTime time.Time) {
|
func (element *AnalogClock) Entity () tomo.Entity {
|
||||||
if newTime == element.time { return }
|
return element.entity
|
||||||
element.time = newTime
|
|
||||||
element.redo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets the element's theme.
|
// Draw causes the element to draw to the specified destination canvas.
|
||||||
func (element *AnalogClock) SetTheme (new tomo.Theme) {
|
func (element *AnalogClock) Draw (destination canvas.Canvas) {
|
||||||
if new == element.theme.Theme { return }
|
bounds := element.entity.Bounds()
|
||||||
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()
|
|
||||||
|
|
||||||
state := tomo.State { }
|
state := tomo.State { }
|
||||||
pattern := element.theme.Pattern(tomo.PatternSunken, state)
|
pattern := element.theme.Pattern(tomo.PatternSunken, state)
|
||||||
padding := element.theme.Padding(tomo.PatternSunken)
|
padding := element.theme.Padding(tomo.PatternSunken)
|
||||||
pattern.Draw(element.core, bounds)
|
pattern.Draw(destination, bounds)
|
||||||
|
|
||||||
bounds = padding.Apply(bounds)
|
bounds = padding.Apply(bounds)
|
||||||
|
|
||||||
@@ -72,6 +46,7 @@ func (element *AnalogClock) draw () {
|
|||||||
|
|
||||||
for hour := 0; hour < 12; hour ++ {
|
for hour := 0; hour < 12; hour ++ {
|
||||||
element.radialLine (
|
element.radialLine (
|
||||||
|
destination,
|
||||||
foreground,
|
foreground,
|
||||||
0.8, 0.9, float64(hour) / 6 * math.Pi)
|
0.8, 0.9, float64(hour) / 6 * math.Pi)
|
||||||
}
|
}
|
||||||
@@ -80,25 +55,40 @@ func (element *AnalogClock) draw () {
|
|||||||
minute := float64(element.time.Minute()) + second / 60
|
minute := float64(element.time.Minute()) + second / 60
|
||||||
hour := float64(element.time.Hour()) + minute / 60
|
hour := float64(element.time.Hour()) + minute / 60
|
||||||
|
|
||||||
element.radialLine(foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
|
element.radialLine(destination, foreground, 0, 0.5, (hour - 3) / 6 * math.Pi)
|
||||||
element.radialLine(foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
|
element.radialLine(destination, foreground, 0, 0.7, (minute - 15) / 30 * math.Pi)
|
||||||
element.radialLine(accent, 0, 0.7, (second - 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 (
|
func (element *AnalogClock) radialLine (
|
||||||
|
destination canvas.Canvas,
|
||||||
source color.RGBA,
|
source color.RGBA,
|
||||||
inner float64,
|
inner float64,
|
||||||
outer float64,
|
outer float64,
|
||||||
radian float64,
|
radian float64,
|
||||||
) {
|
) {
|
||||||
bounds := element.Bounds()
|
bounds := element.entity.Bounds()
|
||||||
width := float64(bounds.Dx()) / 2
|
width := float64(bounds.Dx()) / 2
|
||||||
height := float64(bounds.Dy()) / 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.Cos(radian) * inner * width + width),
|
||||||
int(math.Sin(radian) * inner * height + height)))
|
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.Cos(radian) * outer * width + width),
|
||||||
int(math.Sin(radian) * outer * height + height)))
|
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 "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music"
|
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.
|
// Piano is an element that can be used to input midi notes.
|
||||||
type Piano struct {
|
type Piano struct {
|
||||||
*core.Core
|
entity tomo.FocusableEntity
|
||||||
*core.FocusableCore
|
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
low, high music.Octave
|
|
||||||
|
|
||||||
config config.Wrapped
|
config config.Wrapped
|
||||||
theme theme.Wrapped
|
theme theme.Wrapped
|
||||||
flatTheme theme.Wrapped
|
flatTheme theme.Wrapped
|
||||||
sharpTheme theme.Wrapped
|
sharpTheme theme.Wrapped
|
||||||
|
|
||||||
|
low, high music.Octave
|
||||||
flatKeys []pianoKey
|
flatKeys []pianoKey
|
||||||
sharpKeys []pianoKey
|
sharpKeys []pianoKey
|
||||||
contentBounds image.Rectangle
|
contentBounds image.Rectangle
|
||||||
|
|
||||||
|
enabled bool
|
||||||
pressed *pianoKey
|
pressed *pianoKey
|
||||||
keynavPressed map[music.Note] bool
|
keynavPressed map[music.Note] bool
|
||||||
|
|
||||||
@@ -43,11 +41,7 @@ type Piano struct {
|
|||||||
// NewPiano returns a new piano element with a lowest and highest octave,
|
// NewPiano returns a new piano element with a lowest and highest octave,
|
||||||
// inclusive. If low is greater than high, they will be swapped.
|
// inclusive. If low is greater than high, they will be swapped.
|
||||||
func NewPiano (low, high music.Octave) (element *Piano) {
|
func NewPiano (low, high music.Octave) (element *Piano) {
|
||||||
if low > high {
|
if low > high { low, high = high, low }
|
||||||
temp := low
|
|
||||||
low = high
|
|
||||||
high = temp
|
|
||||||
}
|
|
||||||
|
|
||||||
element = &Piano {
|
element = &Piano {
|
||||||
low: low,
|
low: low,
|
||||||
@@ -58,16 +52,68 @@ func NewPiano (low, high music.Octave) (element *Piano) {
|
|||||||
element.theme.Case = tomo.C("tomo", "piano")
|
element.theme.Case = tomo.C("tomo", "piano")
|
||||||
element.flatTheme.Case = tomo.C("tomo", "piano", "flatKey")
|
element.flatTheme.Case = tomo.C("tomo", "piano", "flatKey")
|
||||||
element.sharpTheme.Case = tomo.C("tomo", "piano", "sharpKey")
|
element.sharpTheme.Case = tomo.C("tomo", "piano", "sharpKey")
|
||||||
element.Core, element.core = core.NewCore (element, func () {
|
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
|
||||||
element.recalculate()
|
|
||||||
element.draw()
|
|
||||||
})
|
|
||||||
element.FocusableCore,
|
|
||||||
element.focusableControl = core.NewFocusableCore(element.core, element.redo)
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
return
|
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.
|
// OnPress sets a function to be called when a key is pressed.
|
||||||
func (element *Piano) OnPress (callback func (note music.Note)) {
|
func (element *Piano) OnPress (callback func (note music.Note)) {
|
||||||
element.onPress = callback
|
element.onPress = callback
|
||||||
@@ -90,7 +136,7 @@ func (element *Piano) HandleMouseUp (x, y int, button input.Button) {
|
|||||||
element.onRelease((*element.pressed).Note)
|
element.onRelease((*element.pressed).Note)
|
||||||
}
|
}
|
||||||
element.pressed = nil
|
element.pressed = nil
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Piano) HandleMotion (x, y int) {
|
func (element *Piano) HandleMotion (x, y int) {
|
||||||
@@ -126,7 +172,7 @@ func (element *Piano) pressUnderMouseCursor (point image.Point) {
|
|||||||
if element.onPress != nil {
|
if element.onPress != nil {
|
||||||
element.onPress((*element.pressed).Note)
|
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 {
|
if element.onPress != nil {
|
||||||
element.onPress(note)
|
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 {
|
if element.onRelease != nil {
|
||||||
element.onRelease(note)
|
element.onRelease(note)
|
||||||
}
|
}
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets the element's theme.
|
// SetTheme sets the element's theme.
|
||||||
@@ -209,8 +255,7 @@ func (element *Piano) SetTheme (new tomo.Theme) {
|
|||||||
element.flatTheme.Theme = new
|
element.flatTheme.Theme = new
|
||||||
element.sharpTheme.Theme = new
|
element.sharpTheme.Theme = new
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.recalculate()
|
element.entity.Invalidate()
|
||||||
element.redo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
@@ -218,13 +263,12 @@ func (element *Piano) SetConfig (new tomo.Config) {
|
|||||||
if new == element.config.Config { return }
|
if new == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.recalculate()
|
element.entity.Invalidate()
|
||||||
element.redo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Piano) updateMinimumSize () {
|
func (element *Piano) updateMinimumSize () {
|
||||||
padding := element.theme.Padding(tomo.PatternPinboard)
|
padding := element.theme.Padding(tomo.PatternPinboard)
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
pianoKeyWidth * 7 * element.countOctaves() +
|
pianoKeyWidth * 7 * element.countOctaves() +
|
||||||
padding.Horizontal(),
|
padding.Horizontal(),
|
||||||
64 + padding.Vertical())
|
64 + padding.Vertical())
|
||||||
@@ -242,19 +286,12 @@ func (element *Piano) countSharps () int {
|
|||||||
return element.countOctaves() * 5
|
return element.countOctaves() * 5
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Piano) redo () {
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *Piano) recalculate () {
|
func (element *Piano) recalculate () {
|
||||||
element.flatKeys = make([]pianoKey, element.countFlats())
|
element.flatKeys = make([]pianoKey, element.countFlats())
|
||||||
element.sharpKeys = make([]pianoKey, element.countSharps())
|
element.sharpKeys = make([]pianoKey, element.countSharps())
|
||||||
|
|
||||||
padding := element.theme.Padding(tomo.PatternPinboard)
|
padding := element.theme.Padding(tomo.PatternPinboard)
|
||||||
bounds := padding.Apply(element.Bounds())
|
bounds := padding.Apply(element.entity.Bounds())
|
||||||
|
|
||||||
dot := bounds.Min
|
dot := bounds.Min
|
||||||
note := element.low.Note(0)
|
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 (
|
func (element *Piano) drawFlat (
|
||||||
|
destination canvas.Canvas,
|
||||||
bounds image.Rectangle,
|
bounds image.Rectangle,
|
||||||
pressed bool,
|
pressed bool,
|
||||||
state tomo.State,
|
state tomo.State,
|
||||||
) {
|
) {
|
||||||
state.Pressed = pressed
|
state.Pressed = pressed
|
||||||
pattern := element.flatTheme.Pattern(tomo.PatternButton, state)
|
pattern := element.flatTheme.Pattern(tomo.PatternButton, state)
|
||||||
pattern.Draw(element.core, bounds)
|
pattern.Draw(destination, bounds)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Piano) drawSharp (
|
func (element *Piano) drawSharp (
|
||||||
|
destination canvas.Canvas,
|
||||||
bounds image.Rectangle,
|
bounds image.Rectangle,
|
||||||
pressed bool,
|
pressed bool,
|
||||||
state tomo.State,
|
state tomo.State,
|
||||||
) {
|
) {
|
||||||
state.Pressed = pressed
|
state.Pressed = pressed
|
||||||
pattern := element.sharpTheme.Pattern(tomo.PatternButton, state)
|
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 "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
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 {
|
type Icon struct {
|
||||||
*core.Core
|
entity tomo.Entity
|
||||||
core core.CoreControl
|
theme theme.Wrapped
|
||||||
theme theme.Wrapped
|
id tomo.Icon
|
||||||
id tomo.Icon
|
size tomo.IconSize
|
||||||
size tomo.IconSize
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Icon creates a new icon element.
|
||||||
func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) {
|
func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) {
|
||||||
element = &Icon {
|
element = &Icon {
|
||||||
id: id,
|
id: id,
|
||||||
size: size,
|
size: size,
|
||||||
}
|
}
|
||||||
|
element.entity = tomo.NewEntity(element)
|
||||||
element.theme.Case = tomo.C("tomo", "icon")
|
element.theme.Case = tomo.C("tomo", "icon")
|
||||||
element.Core, element.core = core.NewCore(element, element.draw)
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
return
|
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) {
|
func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) {
|
||||||
element.id = id
|
element.id = id
|
||||||
element.size = size
|
element.size = size
|
||||||
|
if element.entity == nil { return }
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
if element.core.HasImage() {
|
element.entity.Invalidate()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets the element's theme.
|
// SetTheme sets the element's theme.
|
||||||
func (element *Icon) SetTheme (new tomo.Theme) {
|
func (element *Icon) SetTheme (new tomo.Theme) {
|
||||||
if new == element.theme.Theme { return }
|
if new == element.theme.Theme { return }
|
||||||
element.theme.Theme = new
|
element.theme.Theme = new
|
||||||
|
if element.entity == nil { return }
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
if element.core.HasImage() {
|
element.entity.Invalidate()
|
||||||
element.draw()
|
}
|
||||||
element.core.DamageAll()
|
|
||||||
|
// 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 () {
|
func (element *Icon) updateMinimumSize () {
|
||||||
icon := element.icon()
|
icon := element.icon()
|
||||||
if icon == nil {
|
if icon == nil {
|
||||||
element.core.SetMinimumSize(0, 0)
|
element.entity.SetMinimumSize(0, 0)
|
||||||
} else {
|
} else {
|
||||||
bounds := icon.Bounds()
|
bounds := icon.Bounds()
|
||||||
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
element.entity.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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,35 @@
|
|||||||
package elements
|
package elements
|
||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
|
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 {
|
type Image struct {
|
||||||
*core.Core
|
entity tomo.Entity
|
||||||
core core.CoreControl
|
|
||||||
buffer canvas.Canvas
|
buffer canvas.Canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewImage creates a new image element.
|
||||||
func NewImage (image image.Image) (element *Image) {
|
func NewImage (image image.Image) (element *Image) {
|
||||||
element = &Image { buffer: canvas.FromImage(image) }
|
element = &Image { buffer: canvas.FromImage(image) }
|
||||||
element.Core, element.core = core.NewCore(element, element.draw)
|
element.entity = tomo.NewEntity(element)
|
||||||
bounds := image.Bounds()
|
bounds := element.buffer.Bounds()
|
||||||
element.core.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Image) draw () {
|
// Entity returns this element's entity.
|
||||||
(patterns.Texture { Canvas: element.buffer }).
|
func (element *Image) Entity () tomo.Entity {
|
||||||
Draw(element.core, element.Bounds())
|
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,15 +2,14 @@ package elements
|
|||||||
|
|
||||||
import "golang.org/x/image/math/fixed"
|
import "golang.org/x/image/math/fixed"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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/textdraw"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
// Label is a simple text box.
|
// Label is a simple text box.
|
||||||
type Label struct {
|
type Label struct {
|
||||||
*core.Core
|
entity tomo.FlexibleEntity
|
||||||
core core.CoreControl
|
|
||||||
|
|
||||||
align textdraw.Align
|
align textdraw.Align
|
||||||
wrap bool
|
wrap bool
|
||||||
@@ -19,50 +18,34 @@ type Label struct {
|
|||||||
|
|
||||||
forcedColumns int
|
forcedColumns int
|
||||||
forcedRows int
|
forcedRows int
|
||||||
|
minHeight int
|
||||||
|
|
||||||
config config.Wrapped
|
config config.Wrapped
|
||||||
theme theme.Wrapped
|
theme theme.Wrapped
|
||||||
|
|
||||||
onFlexibleHeightChange func ()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLabel creates a new label. If wrap is set to true, the text inside will be
|
// NewLabel creates a new label.
|
||||||
// wrapped.
|
func NewLabel (text string) (element *Label) {
|
||||||
func NewLabel (text string, wrap bool) (element *Label) {
|
|
||||||
element = &Label { }
|
element = &Label { }
|
||||||
element.theme.Case = tomo.C("tomo", "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 (
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
tomo.FontStyleRegular,
|
tomo.FontStyleRegular,
|
||||||
tomo.FontSizeNormal))
|
tomo.FontSizeNormal))
|
||||||
element.SetWrap(wrap)
|
|
||||||
element.SetText(text)
|
element.SetText(text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Label) redo () {
|
// NewLabelWrapped creates a new label with text wrapping on.
|
||||||
face := element.theme.FontFace (
|
func NewLabelWrapped (text string) (element *Label) {
|
||||||
tomo.FontStyleRegular,
|
element = NewLabel(text)
|
||||||
tomo.FontSizeNormal)
|
element.SetWrap(true)
|
||||||
element.drawer.SetFace(face)
|
return
|
||||||
element.updateMinimumSize()
|
|
||||||
bounds := element.Bounds()
|
|
||||||
if element.wrap {
|
|
||||||
element.drawer.SetMaxWidth(bounds.Dx())
|
|
||||||
element.drawer.SetMaxHeight(bounds.Dy())
|
|
||||||
}
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Label) handleResize () {
|
// Entity returns this element's entity.
|
||||||
bounds := element.Bounds()
|
func (element *Label) Entity () tomo.Entity {
|
||||||
if element.wrap {
|
return element.entity
|
||||||
element.drawer.SetMaxWidth(bounds.Dx())
|
|
||||||
element.drawer.SetMaxHeight(bounds.Dy())
|
|
||||||
}
|
|
||||||
element.draw()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// EmCollapse forces a minimum width and height upon the label. The width is
|
// 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 {
|
if element.wrap {
|
||||||
return element.drawer.ReccomendedHeightFor(width)
|
return element.drawer.ReccomendedHeightFor(width)
|
||||||
} else {
|
} else {
|
||||||
_, height = element.MinimumSize()
|
return element.minHeight
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// SetText sets the label's text.
|
||||||
func (element *Label) SetText (text string) {
|
func (element *Label) SetText (text string) {
|
||||||
if element.text == text { return }
|
if element.text == text { return }
|
||||||
@@ -100,11 +76,7 @@ func (element *Label) SetText (text string) {
|
|||||||
element.text = text
|
element.text = text
|
||||||
element.drawer.SetText([]rune(text))
|
element.drawer.SetText([]rune(text))
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetWrap sets wether or not the label's text wraps. If the text is set to
|
// 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.wrap = wrap
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetAlign sets the alignment method of the label.
|
// SetAlign sets the alignment method of the label.
|
||||||
func (element *Label) SetAlign (align textdraw.Align) {
|
func (element *Label) SetAlign (align textdraw.Align) {
|
||||||
if align == element.align { return }
|
if align == element.align { return }
|
||||||
|
|
||||||
element.align = align
|
element.align = align
|
||||||
element.drawer.SetAlign(align)
|
element.drawer.SetAlign(align)
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets the element's theme.
|
// SetTheme sets the element's theme.
|
||||||
@@ -148,11 +111,7 @@ func (element *Label) SetTheme (new tomo.Theme) {
|
|||||||
tomo.FontStyleRegular,
|
tomo.FontStyleRegular,
|
||||||
tomo.FontSizeNormal))
|
tomo.FontSizeNormal))
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
@@ -160,11 +119,25 @@ func (element *Label) SetConfig (new tomo.Config) {
|
|||||||
if new == element.config.Config { return }
|
if new == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
if element.core.HasImage () {
|
// Draw causes the element to draw to the specified destination canvas.
|
||||||
element.draw()
|
func (element *Label) Draw (destination canvas.Canvas) {
|
||||||
element.core.DamageAll()
|
bounds := element.entity.Bounds()
|
||||||
|
|
||||||
|
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 () {
|
func (element *Label) updateMinimumSize () {
|
||||||
@@ -176,9 +149,7 @@ func (element *Label) updateMinimumSize () {
|
|||||||
em = element.theme.Padding(tomo.PatternBackground)[0]
|
em = element.theme.Padding(tomo.PatternBackground)[0]
|
||||||
}
|
}
|
||||||
width, height = em, element.drawer.LineHeight().Round()
|
width, height = em, element.drawer.LineHeight().Round()
|
||||||
if element.onFlexibleHeightChange != nil {
|
element.entity.NotifyFlexibleHeightChange()
|
||||||
element.onFlexibleHeightChange()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
bounds := element.drawer.LayoutBounds()
|
bounds := element.drawer.LayoutBounds()
|
||||||
width, height = bounds.Dx(), bounds.Dy()
|
width, height = bounds.Dx(), bounds.Dy()
|
||||||
@@ -196,18 +167,6 @@ func (element *Label) updateMinimumSize () {
|
|||||||
Mul(fixed.I(element.forcedRows)).Floor()
|
Mul(fixed.I(element.forcedRows)).Floor()
|
||||||
}
|
}
|
||||||
|
|
||||||
element.core.SetMinimumSize(width, height)
|
element.minHeight = height
|
||||||
}
|
element.entity.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))
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,16 +15,20 @@ type LerpSlider[T Numeric] struct {
|
|||||||
max T
|
max T
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. If
|
// NewLerpSlider creates a new LerpSlider with a minimum and maximum value.
|
||||||
// vertical is set to true, the slider will be vertical instead of horizontal.
|
func NewLerpSlider[T Numeric] (
|
||||||
func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *LerpSlider[T]) {
|
min, max T, value T,
|
||||||
|
orientation Orientation,
|
||||||
|
) (
|
||||||
|
element *LerpSlider[T],
|
||||||
|
) {
|
||||||
if min > max {
|
if min > max {
|
||||||
temp := max
|
temp := max
|
||||||
max = min
|
max = min
|
||||||
min = temp
|
min = temp
|
||||||
}
|
}
|
||||||
element = &LerpSlider[T] {
|
element = &LerpSlider[T] {
|
||||||
Slider: NewSlider(0, vertical),
|
Slider: NewSlider(0, orientation),
|
||||||
min: min,
|
min: min,
|
||||||
max: max,
|
max: max,
|
||||||
}
|
}
|
||||||
|
|||||||
596
elements/list.go
596
elements/list.go
@@ -1,107 +1,148 @@
|
|||||||
package elements
|
package elements
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
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/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 {
|
type List struct {
|
||||||
*core.Core
|
container
|
||||||
*core.FocusableCore
|
entity listEntity
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
|
|
||||||
pressed bool
|
scroll image.Point
|
||||||
|
contentBounds image.Rectangle
|
||||||
|
columnSizes []int
|
||||||
|
selected int
|
||||||
|
|
||||||
contentHeight int
|
|
||||||
forcedMinimumWidth int
|
forcedMinimumWidth int
|
||||||
forcedMinimumHeight int
|
forcedMinimumHeight int
|
||||||
|
|
||||||
selectedEntry int
|
theme theme.Wrapped
|
||||||
scroll int
|
|
||||||
entries []ListEntry
|
|
||||||
|
|
||||||
config config.Wrapped
|
|
||||||
theme theme.Wrapped
|
|
||||||
|
|
||||||
onNoEntrySelected func ()
|
|
||||||
onScrollBoundsChange func ()
|
onScrollBoundsChange func ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewList creates a new list element with the specified entries.
|
func NewList (columns int, children ...tomo.Element) (element *List) {
|
||||||
func NewList (entries ...ListEntry) (element *List) {
|
if columns < 1 { columns = 1 }
|
||||||
element = &List { selectedEntry: -1 }
|
element = &List { selected: -1 }
|
||||||
|
element.columnSizes = make([]int, columns)
|
||||||
element.theme.Case = tomo.C("tomo", "list")
|
element.theme.Case = tomo.C("tomo", "list")
|
||||||
element.Core, element.core = core.NewCore(element, element.handleResize)
|
element.entity = tomo.NewEntity(element).(listEntity)
|
||||||
element.FocusableCore,
|
element.container.entity = element.entity
|
||||||
element.focusableControl = core.NewFocusableCore (element.core, func () {
|
element.minimumSize = element.updateMinimumSize
|
||||||
if element.core.HasImage () {
|
element.init()
|
||||||
element.draw()
|
element.Adopt(children...)
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
element.entries = make([]ListEntry, len(entries))
|
|
||||||
for index, entry := range entries {
|
|
||||||
element.entries[index] = entry
|
|
||||||
}
|
|
||||||
|
|
||||||
element.updateMinimumSize()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *List) handleResize () {
|
func (element *List) Draw (destination canvas.Canvas) {
|
||||||
for index, entry := range element.entries {
|
rocks := make([]image.Rectangle, element.entity.CountChildren())
|
||||||
element.entries[index] = element.resizeEntryToFit(entry)
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
|
rocks[index] = element.entity.Child(index).Entity().Bounds()
|
||||||
}
|
}
|
||||||
|
|
||||||
if element.scroll > element.maxScrollHeight() {
|
pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { })
|
||||||
element.scroll = element.maxScrollHeight()
|
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.
|
// SetTheme sets the element's theme.
|
||||||
func (element *List) SetTheme (new tomo.Theme) {
|
func (element *List) SetTheme (theme tomo.Theme) {
|
||||||
if new == element.theme.Theme { return }
|
if theme == element.theme.Theme { return }
|
||||||
element.theme.Theme = new
|
element.theme.Theme = theme
|
||||||
for index, entry := range element.entries {
|
|
||||||
entry.SetTheme(element.theme.Theme)
|
|
||||||
element.entries[index] = entry
|
|
||||||
}
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
element.entity.InvalidateLayout()
|
||||||
|
|
||||||
// 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()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Collapse forces a minimum width and height upon the list. If a zero value is
|
// 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.forcedMinimumWidth = width
|
||||||
element.forcedMinimumHeight = height
|
element.forcedMinimumHeight = height
|
||||||
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
for index, entry := range element.entries {
|
element.entity.InvalidateLayout()
|
||||||
element.entries[index] = element.resizeEntryToFit(entry)
|
|
||||||
}
|
|
||||||
|
|
||||||
element.redo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *List) HandleMouseDown (x, y int, button input.Button) {
|
|
||||||
if !element.Enabled() { return }
|
|
||||||
if !element.Focused() { element.Focus() }
|
|
||||||
if button != input.ButtonLeft { return }
|
|
||||||
element.pressed = true
|
|
||||||
if element.selectUnderMouse(x, y) && element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *List) HandleMouseUp (x, y int, button input.Button) {
|
|
||||||
if button != input.ButtonLeft { return }
|
|
||||||
element.pressed = false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *List) HandleMotion (x, y int) {
|
|
||||||
if element.pressed {
|
|
||||||
if element.selectUnderMouse(x, y) && element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
|
||||||
if !element.Enabled() { return }
|
|
||||||
|
|
||||||
altered := false
|
|
||||||
switch key {
|
|
||||||
case input.KeyLeft, input.KeyUp:
|
|
||||||
altered = element.changeSelectionBy(-1)
|
|
||||||
|
|
||||||
case input.KeyRight, input.KeyDown:
|
|
||||||
altered = element.changeSelectionBy(1)
|
|
||||||
|
|
||||||
case input.KeyEscape:
|
|
||||||
altered = element.selectEntry(-1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if altered && element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
|
||||||
|
|
||||||
// ScrollContentBounds returns the full content size of the element.
|
// ScrollContentBounds returns the full content size of the element.
|
||||||
func (element *List) ScrollContentBounds () (bounds image.Rectangle) {
|
func (element *List) ScrollContentBounds () image.Rectangle {
|
||||||
return image.Rect (
|
return element.contentBounds
|
||||||
0, 0,
|
|
||||||
1, element.contentHeight)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScrollViewportBounds returns the size and position of the element's viewport
|
// ScrollViewportBounds returns the size and position of the element's
|
||||||
// relative to ScrollBounds.
|
// viewport relative to ScrollBounds.
|
||||||
func (element *List) ScrollViewportBounds () (bounds image.Rectangle) {
|
func (element *List) ScrollViewportBounds () image.Rectangle {
|
||||||
return image.Rect (
|
padding := element.theme.Padding(tomo.PatternBackground)
|
||||||
0, element.scroll,
|
bounds := padding.Apply(element.entity.Bounds())
|
||||||
0, element.scroll + element.scrollViewportHeight())
|
bounds = bounds.Sub(bounds.Min).Add(element.scroll)
|
||||||
|
return bounds
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScrollTo scrolls the viewport to the specified point relative to
|
// ScrollTo scrolls the viewport to the specified point relative to
|
||||||
// ScrollBounds.
|
// ScrollBounds.
|
||||||
func (element *List) ScrollTo (position image.Point) {
|
func (element *List) ScrollTo (position image.Point) {
|
||||||
element.scroll = position.Y
|
if position.Y < 0 {
|
||||||
if element.scroll < 0 {
|
position.Y = 0
|
||||||
element.scroll = 0
|
|
||||||
} else if element.scroll > element.maxScrollHeight() {
|
|
||||||
element.scroll = element.maxScrollHeight()
|
|
||||||
}
|
}
|
||||||
|
maxScrollHeight := element.maxScrollHeight()
|
||||||
if element.core.HasImage () {
|
if position.Y > maxScrollHeight {
|
||||||
element.draw()
|
position.Y = maxScrollHeight
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
}
|
||||||
element.scrollBoundsChange()
|
element.scroll = position
|
||||||
}
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnScrollBoundsChange sets a function to be called when the element's viewport
|
// 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
|
element.onScrollBoundsChange = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
// CountEntries returns the amount of entries in the list.
|
// ScrollAxes returns the supported axes for scrolling.
|
||||||
func (element *List) CountEntries () (count int) {
|
func (element *List) ScrollAxes () (horizontal, vertical bool) {
|
||||||
return len(element.entries)
|
return false, true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append adds one or more entries to the end of the list.
|
func (element *List) maxScrollHeight () (height int) {
|
||||||
func (element *List) Append (entries ...ListEntry) {
|
|
||||||
// append
|
|
||||||
for index, entry := range entries {
|
|
||||||
entry = element.resizeEntryToFit(entry)
|
|
||||||
entry.SetTheme(element.theme.Theme)
|
|
||||||
entry.SetConfig(element.config)
|
|
||||||
entries[index] = entry
|
|
||||||
}
|
|
||||||
element.entries = append(element.entries, entries...)
|
|
||||||
|
|
||||||
// recalculate, redraw, notify
|
|
||||||
element.updateMinimumSize()
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
element.scrollBoundsChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// EntryAt returns the entry at the specified index. If the index is out of
|
|
||||||
// bounds, it panics.
|
|
||||||
func (element *List) EntryAt (index int) (entry ListEntry) {
|
|
||||||
if index < 0 || index >= len(element.entries) {
|
|
||||||
panic(fmt.Sprint("basic.List.EntryAt index out of range: ", index))
|
|
||||||
}
|
|
||||||
return element.entries[index]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Insert inserts an entry into the list at the speified index. If the index is
|
|
||||||
// out of bounds, it is constrained either to zero or len(entries).
|
|
||||||
func (element *List) Insert (index int, entry ListEntry) {
|
|
||||||
if index < 0 { index = 0 }
|
|
||||||
if index > len(element.entries) { index = len(element.entries) }
|
|
||||||
|
|
||||||
// insert
|
|
||||||
element.entries = append (
|
|
||||||
element.entries[:index + 1],
|
|
||||||
element.entries[index:]...)
|
|
||||||
entry = element.resizeEntryToFit(entry)
|
|
||||||
element.entries[index] = entry
|
|
||||||
|
|
||||||
// recalculate, redraw, notify
|
|
||||||
element.updateMinimumSize()
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
element.scrollBoundsChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove removes the entry at the specified index. If the index is out of
|
|
||||||
// bounds, it panics.
|
|
||||||
func (element *List) Remove (index int) {
|
|
||||||
if index < 0 || index >= len(element.entries) {
|
|
||||||
panic(fmt.Sprint("basic.List.Remove index out of range: ", index))
|
|
||||||
}
|
|
||||||
|
|
||||||
// delete
|
|
||||||
element.entries = append (
|
|
||||||
element.entries[:index],
|
|
||||||
element.entries[index + 1:]...)
|
|
||||||
|
|
||||||
// recalculate, redraw, notify
|
|
||||||
element.updateMinimumSize()
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
element.scrollBoundsChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear removes all entries from the list.
|
|
||||||
func (element *List) Clear () {
|
|
||||||
element.entries = nil
|
|
||||||
|
|
||||||
// recalculate, redraw, notify
|
|
||||||
element.updateMinimumSize()
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
element.scrollBoundsChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Replace replaces the entry at the specified index with another. If the index
|
|
||||||
// is out of bounds, it panics.
|
|
||||||
func (element *List) Replace (index int, entry ListEntry) {
|
|
||||||
if index < 0 || index >= len(element.entries) {
|
|
||||||
panic(fmt.Sprint("basic.List.Replace index out of range: ", index))
|
|
||||||
}
|
|
||||||
|
|
||||||
// replace
|
|
||||||
entry = element.resizeEntryToFit(entry)
|
|
||||||
element.entries[index] = entry
|
|
||||||
|
|
||||||
// redraw
|
|
||||||
element.updateMinimumSize()
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
element.scrollBoundsChange()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Select selects a specific item in the list. If the index is out of bounds,
|
|
||||||
// no items will be selecected.
|
|
||||||
func (element *List) Select (index int) {
|
|
||||||
if element.selectEntry(index) {
|
|
||||||
element.redo()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *List) selectUnderMouse (x, y int) (updated bool) {
|
|
||||||
padding := element.theme.Padding(tomo.PatternSunken)
|
padding := element.theme.Padding(tomo.PatternSunken)
|
||||||
bounds := padding.Apply(element.Bounds())
|
viewportHeight := element.entity.Bounds().Dy() - padding.Vertical()
|
||||||
mousePoint := image.Pt(x, y)
|
height = element.contentBounds.Dy() - viewportHeight
|
||||||
dot := image.Pt (
|
if height < 0 { height = 0 }
|
||||||
bounds.Min.X,
|
return
|
||||||
bounds.Min.Y - element.scroll)
|
|
||||||
|
|
||||||
newlySelectedEntryIndex := -1
|
|
||||||
for index, entry := range element.entries {
|
|
||||||
entryPosition := dot
|
|
||||||
dot.Y += entry.Bounds().Dy()
|
|
||||||
if entryPosition.Y > bounds.Max.Y { break }
|
|
||||||
if mousePoint.In(entry.Bounds().Add(entryPosition)) {
|
|
||||||
newlySelectedEntryIndex = index
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return element.selectEntry(newlySelectedEntryIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *List) selectEntry (index int) (updated bool) {
|
|
||||||
if element.selectedEntry == index { return false }
|
|
||||||
element.selectedEntry = index
|
|
||||||
if element.selectedEntry < 0 {
|
|
||||||
if element.onNoEntrySelected != nil {
|
|
||||||
element.onNoEntrySelected()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
element.entries[element.selectedEntry].RunSelect()
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *List) changeSelectionBy (delta int) (updated bool) {
|
|
||||||
newIndex := element.selectedEntry + delta
|
|
||||||
if newIndex < 0 { newIndex = len(element.entries) - 1 }
|
|
||||||
if newIndex >= len(element.entries) { newIndex = 0 }
|
|
||||||
return element.selectEntry(newIndex)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) {
|
|
||||||
bounds := element.Bounds()
|
|
||||||
padding := element.theme.Padding(tomo.PatternSunken)
|
|
||||||
entry.Resize(padding.Apply(bounds).Dx())
|
|
||||||
return entry
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *List) updateMinimumSize () {
|
func (element *List) updateMinimumSize () {
|
||||||
element.contentHeight = 0
|
margin := element.theme.Margin(tomo.PatternSunken)
|
||||||
for _, entry := range element.entries {
|
|
||||||
element.contentHeight += entry.Bounds().Dy()
|
|
||||||
}
|
|
||||||
|
|
||||||
minimumWidth := element.forcedMinimumWidth
|
|
||||||
minimumHeight := element.forcedMinimumHeight
|
|
||||||
|
|
||||||
if minimumWidth == 0 {
|
|
||||||
for _, entry := range element.entries {
|
|
||||||
entryWidth := entry.MinimumWidth()
|
|
||||||
if entryWidth > minimumWidth {
|
|
||||||
minimumWidth = entryWidth
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if minimumHeight == 0 {
|
|
||||||
minimumHeight = element.contentHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
padding := element.theme.Padding(tomo.PatternSunken)
|
padding := element.theme.Padding(tomo.PatternSunken)
|
||||||
minimumHeight += padding[0] + padding[2]
|
|
||||||
|
|
||||||
element.core.SetMinimumSize(minimumWidth, minimumHeight)
|
for index := range element.columnSizes {
|
||||||
}
|
element.columnSizes[index] = 0
|
||||||
|
}
|
||||||
func (element *List) scrollBoundsChange () {
|
|
||||||
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
|
height := 0
|
||||||
parent.NotifyScrollBoundsChange(element)
|
rowHeight := 0
|
||||||
}
|
columnIndex := 0
|
||||||
if element.onScrollBoundsChange != nil {
|
nextLine := func () {
|
||||||
element.onScrollBoundsChange()
|
height += rowHeight
|
||||||
}
|
rowHeight = 0
|
||||||
}
|
columnIndex = 0
|
||||||
|
}
|
||||||
func (element *List) draw () {
|
for index := 0; index < element.entity.CountChildren(); index ++ {
|
||||||
bounds := element.Bounds()
|
if columnIndex >= len(element.columnSizes) {
|
||||||
padding := element.theme.Padding(tomo.PatternSunken)
|
if index > 0 { height += margin.Y }
|
||||||
innerBounds := padding.Apply(bounds)
|
nextLine()
|
||||||
state := tomo.State {
|
}
|
||||||
Disabled: !element.Enabled(),
|
|
||||||
Focused: element.Focused(),
|
child := element.entity.Child(index)
|
||||||
}
|
entry := element.scratch[child]
|
||||||
|
|
||||||
dot := image.Point {
|
entryWidth, entryHeight := element.entity.ChildMinimumSize(index)
|
||||||
innerBounds.Min.X,
|
entry.minBreadth = float64(entryWidth)
|
||||||
innerBounds.Min.Y - element.scroll,
|
entry.minSize = float64(entryHeight)
|
||||||
}
|
element.scratch[child] = entry
|
||||||
innerCanvas := canvas.Cut(element.core, innerBounds)
|
|
||||||
for index, entry := range element.entries {
|
if rowHeight < entryHeight {
|
||||||
entryPosition := dot
|
rowHeight = entryHeight
|
||||||
dot.Y += entry.Bounds().Dy()
|
}
|
||||||
if dot.Y < innerBounds.Min.Y { continue }
|
if element.columnSizes[columnIndex] < entryWidth {
|
||||||
if entryPosition.Y > innerBounds.Max.Y { break }
|
element.columnSizes[columnIndex] = entryWidth
|
||||||
entry.Draw (
|
}
|
||||||
innerCanvas, entryPosition,
|
|
||||||
element.Focused(), element.selectedEntry == index)
|
columnIndex ++
|
||||||
}
|
}
|
||||||
|
nextLine()
|
||||||
covered := image.Rect (
|
|
||||||
0, 0,
|
width := 0; for index, size := range element.columnSizes {
|
||||||
innerBounds.Dx(), element.contentHeight,
|
width += size
|
||||||
).Add(innerBounds.Min).Intersect(innerBounds)
|
if index > 0 { width += margin.X }
|
||||||
pattern := element.theme.Pattern(tomo.PatternSunken, state)
|
}
|
||||||
artist.DrawShatter (
|
width += padding.Horizontal()
|
||||||
element.core, pattern, bounds, covered)
|
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 "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
// ProgressBar displays a visual indication of how far along a task is.
|
// ProgressBar displays a visual indication of how far along a task is.
|
||||||
type ProgressBar struct {
|
type ProgressBar struct {
|
||||||
*core.Core
|
entity tomo.Entity
|
||||||
core core.CoreControl
|
|
||||||
progress float64
|
progress float64
|
||||||
|
|
||||||
config config.Wrapped
|
config config.Wrapped
|
||||||
@@ -19,21 +18,43 @@ type ProgressBar struct {
|
|||||||
// NewProgressBar creates a new progress bar displaying the given progress
|
// NewProgressBar creates a new progress bar displaying the given progress
|
||||||
// level.
|
// level.
|
||||||
func NewProgressBar (progress float64) (element *ProgressBar) {
|
func NewProgressBar (progress float64) (element *ProgressBar) {
|
||||||
|
if progress < 0 { progress = 0 }
|
||||||
|
if progress > 1 { progress = 1 }
|
||||||
element = &ProgressBar { progress: progress }
|
element = &ProgressBar { progress: progress }
|
||||||
|
element.entity = tomo.NewEntity(element)
|
||||||
element.theme.Case = tomo.C("tomo", "progressBar")
|
element.theme.Case = tomo.C("tomo", "progressBar")
|
||||||
element.Core, element.core = core.NewCore(element, element.draw)
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
return
|
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.
|
// SetProgress sets the progress level of the bar.
|
||||||
func (element *ProgressBar) SetProgress (progress float64) {
|
func (element *ProgressBar) SetProgress (progress float64) {
|
||||||
|
if progress < 0 { progress = 0 }
|
||||||
|
if progress > 1 { progress = 1 }
|
||||||
if progress == element.progress { return }
|
if progress == element.progress { return }
|
||||||
element.progress = progress
|
element.progress = progress
|
||||||
if element.core.HasImage() {
|
element.entity.Invalidate()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets the element's theme.
|
// SetTheme sets the element's theme.
|
||||||
@@ -41,7 +62,7 @@ func (element *ProgressBar) SetTheme (new tomo.Theme) {
|
|||||||
if new == element.theme.Theme { return }
|
if new == element.theme.Theme { return }
|
||||||
element.theme.Theme = new
|
element.theme.Theme = new
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// 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 }
|
if new == nil || new == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *ProgressBar) updateMinimumSize() {
|
func (element *ProgressBar) updateMinimumSize() {
|
||||||
padding := element.theme.Padding(tomo.PatternSunken)
|
padding := element.theme.Padding(tomo.PatternSunken)
|
||||||
innerPadding := element.theme.Padding(tomo.PatternMercury)
|
innerPadding := element.theme.Padding(tomo.PatternMercury)
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
padding.Horizontal() + innerPadding.Horizontal(),
|
padding.Horizontal() + innerPadding.Horizontal(),
|
||||||
padding.Vertical() + innerPadding.Vertical())
|
padding.Vertical() + innerPadding.Vertical())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *ProgressBar) redo () {
|
|
||||||
if element.core.HasImage() {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *ProgressBar) draw () {
|
|
||||||
bounds := element.Bounds()
|
|
||||||
|
|
||||||
pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { })
|
|
||||||
padding := element.theme.Padding(tomo.PatternSunken)
|
|
||||||
pattern.Draw(element.core, bounds)
|
|
||||||
bounds = padding.Apply(bounds)
|
|
||||||
meterBounds := image.Rect (
|
|
||||||
bounds.Min.X, bounds.Min.Y,
|
|
||||||
bounds.Min.X + int(float64(bounds.Dx()) * element.progress),
|
|
||||||
bounds.Max.Y)
|
|
||||||
mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { })
|
|
||||||
mercury.Draw(element.core, meterBounds)
|
|
||||||
}
|
|
||||||
|
|||||||
232
elements/scroll.go
Normal file
232
elements/scroll.go
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
package elements
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
// import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
|
type ScrollMode int; const (
|
||||||
|
ScrollNeither ScrollMode = 0
|
||||||
|
ScrollVertical = 1
|
||||||
|
ScrollHorizontal = 2
|
||||||
|
ScrollBoth = ScrollVertical | ScrollHorizontal
|
||||||
|
)
|
||||||
|
|
||||||
|
// Includes returns whether a scroll mode has been or'd with another scroll
|
||||||
|
// mode.
|
||||||
|
func (mode ScrollMode) Includes (sub ScrollMode) bool {
|
||||||
|
return (mode & sub) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
type Scroll struct {
|
||||||
|
entity tomo.ContainerEntity
|
||||||
|
|
||||||
|
child tomo.Scrollable
|
||||||
|
horizontal *ScrollBar
|
||||||
|
vertical *ScrollBar
|
||||||
|
|
||||||
|
config config.Wrapped
|
||||||
|
theme theme.Wrapped
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewScroll (mode ScrollMode, child tomo.Scrollable) (element *Scroll) {
|
||||||
|
element = &Scroll { }
|
||||||
|
element.theme.Case = tomo.C("tomo", "scroll")
|
||||||
|
element.entity = tomo.NewEntity(element).(tomo.ContainerEntity)
|
||||||
|
|
||||||
|
if mode.Includes(ScrollHorizontal) {
|
||||||
|
element.horizontal = NewScrollBar(false)
|
||||||
|
element.horizontal.OnScroll (func (viewport image.Point) {
|
||||||
|
if element.child != nil {
|
||||||
|
element.child.ScrollTo(viewport)
|
||||||
|
}
|
||||||
|
if element.vertical != nil {
|
||||||
|
element.vertical.SetBounds (
|
||||||
|
element.child.ScrollContentBounds(),
|
||||||
|
element.child.ScrollViewportBounds())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
element.entity.Adopt(element.horizontal)
|
||||||
|
}
|
||||||
|
if mode.Includes(ScrollVertical) {
|
||||||
|
element.vertical = NewScrollBar(true)
|
||||||
|
element.vertical.OnScroll (func (viewport image.Point) {
|
||||||
|
if element.child != nil {
|
||||||
|
element.child.ScrollTo(viewport)
|
||||||
|
}
|
||||||
|
if element.horizontal != nil {
|
||||||
|
element.horizontal.SetBounds (
|
||||||
|
element.child.ScrollContentBounds(),
|
||||||
|
element.child.ScrollViewportBounds())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
element.entity.Adopt(element.vertical)
|
||||||
|
}
|
||||||
|
|
||||||
|
element.Adopt(child)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) Entity () tomo.Entity {
|
||||||
|
return element.entity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) Draw (destination canvas.Canvas) {
|
||||||
|
if element.horizontal != nil && element.vertical != nil {
|
||||||
|
bounds := element.entity.Bounds()
|
||||||
|
bounds.Min = image.Pt (
|
||||||
|
bounds.Max.X - element.vertical.Entity().Bounds().Dx(),
|
||||||
|
bounds.Max.Y - element.horizontal.Entity().Bounds().Dy())
|
||||||
|
state := tomo.State { }
|
||||||
|
deadArea := element.theme.Pattern(tomo.PatternDead, state)
|
||||||
|
deadArea.Draw(canvas.Cut(destination, bounds), bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) Layout () {
|
||||||
|
bounds := element.entity.Bounds()
|
||||||
|
child := bounds
|
||||||
|
|
||||||
|
iHorizontal := element.entity.IndexOf(element.horizontal)
|
||||||
|
iVertical := element.entity.IndexOf(element.vertical)
|
||||||
|
iChild := element.entity.IndexOf(element.child)
|
||||||
|
|
||||||
|
var horizontal, vertical image.Rectangle
|
||||||
|
|
||||||
|
if element.horizontal != nil {
|
||||||
|
_, hMinHeight := element.entity.ChildMinimumSize(iHorizontal)
|
||||||
|
child.Max.Y -= hMinHeight
|
||||||
|
}
|
||||||
|
if element.vertical != nil {
|
||||||
|
vMinWidth, _ := element.entity.ChildMinimumSize(iVertical)
|
||||||
|
child.Max.X -= vMinWidth
|
||||||
|
}
|
||||||
|
|
||||||
|
horizontal.Min.X = bounds.Min.X
|
||||||
|
horizontal.Max.X = child.Max.X
|
||||||
|
horizontal.Min.Y = child.Max.Y
|
||||||
|
horizontal.Max.Y = bounds.Max.Y
|
||||||
|
|
||||||
|
vertical.Min.X = child.Max.X
|
||||||
|
vertical.Max.X = bounds.Max.X
|
||||||
|
vertical.Min.Y = bounds.Min.Y
|
||||||
|
vertical.Max.Y = child.Max.Y
|
||||||
|
|
||||||
|
if element.horizontal != nil {
|
||||||
|
element.entity.PlaceChild (iHorizontal, horizontal)
|
||||||
|
}
|
||||||
|
if element.vertical != nil {
|
||||||
|
element.entity.PlaceChild(iVertical, vertical)
|
||||||
|
}
|
||||||
|
if element.child != nil {
|
||||||
|
element.entity.PlaceChild(iChild, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) DrawBackground (destination canvas.Canvas) {
|
||||||
|
element.entity.DrawBackground(destination)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) Adopt (child tomo.Scrollable) {
|
||||||
|
if element.child != nil {
|
||||||
|
element.entity.Disown(element.entity.IndexOf(element.child))
|
||||||
|
}
|
||||||
|
if child != nil {
|
||||||
|
element.entity.Adopt(child)
|
||||||
|
}
|
||||||
|
element.child = child
|
||||||
|
|
||||||
|
element.updateEnabled()
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) {
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) HandleChildScrollBoundsChange (tomo.Scrollable) {
|
||||||
|
element.updateEnabled()
|
||||||
|
viewportBounds := element.child.ScrollViewportBounds()
|
||||||
|
contentBounds := element.child.ScrollContentBounds()
|
||||||
|
if element.horizontal != nil {
|
||||||
|
element.horizontal.SetBounds(contentBounds, viewportBounds)
|
||||||
|
}
|
||||||
|
if element.vertical != nil {
|
||||||
|
element.vertical.SetBounds(contentBounds, viewportBounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) HandleScroll (
|
||||||
|
x, y int,
|
||||||
|
deltaX, deltaY float64,
|
||||||
|
) {
|
||||||
|
horizontal, vertical := element.child.ScrollAxes()
|
||||||
|
if !horizontal { deltaX = 0 }
|
||||||
|
if !vertical { deltaY = 0 }
|
||||||
|
element.scrollChildBy(int(deltaX), int(deltaY))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) SetTheme (theme tomo.Theme) {
|
||||||
|
if theme == element.theme.Theme { return }
|
||||||
|
element.theme.Theme = theme
|
||||||
|
element.updateMinimumSize()
|
||||||
|
element.entity.Invalidate()
|
||||||
|
element.entity.InvalidateLayout()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) SetConfig (config tomo.Config) {
|
||||||
|
element.config.Config = config
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) updateMinimumSize () {
|
||||||
|
var width, height int
|
||||||
|
|
||||||
|
if element.child != nil {
|
||||||
|
width, height = element.entity.ChildMinimumSize (
|
||||||
|
element.entity.IndexOf(element.child))
|
||||||
|
}
|
||||||
|
if element.horizontal != nil {
|
||||||
|
hMinWidth, hMinHeight := element.entity.ChildMinimumSize (
|
||||||
|
element.entity.IndexOf(element.horizontal))
|
||||||
|
height += hMinHeight
|
||||||
|
if hMinWidth > width {
|
||||||
|
width = hMinWidth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if element.vertical != nil {
|
||||||
|
vMinWidth, vMinHeight := element.entity.ChildMinimumSize (
|
||||||
|
element.entity.IndexOf(element.vertical))
|
||||||
|
width += vMinWidth
|
||||||
|
if vMinHeight > height {
|
||||||
|
height = vMinHeight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
element.entity.SetMinimumSize(width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) updateEnabled () {
|
||||||
|
horizontal, vertical := false, false
|
||||||
|
if element.child != nil {
|
||||||
|
horizontal, vertical = element.child.ScrollAxes()
|
||||||
|
}
|
||||||
|
if element.horizontal != nil {
|
||||||
|
element.horizontal.SetEnabled(horizontal)
|
||||||
|
}
|
||||||
|
if element.vertical != nil {
|
||||||
|
element.vertical.SetEnabled(vertical)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (element *Scroll) scrollChildBy (x, y int) {
|
||||||
|
if element.child == nil { return }
|
||||||
|
scrollPoint :=
|
||||||
|
element.child.ScrollViewportBounds().Min.
|
||||||
|
Add(image.Pt(x, y))
|
||||||
|
element.child.ScrollTo(scrollPoint)
|
||||||
|
}
|
||||||
@@ -3,10 +3,17 @@ package elements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
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
|
// 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
|
// 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
|
// 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
|
// Typically, you wont't want to use a ScrollBar by itself. A ScrollContainer is
|
||||||
// better for most cases.
|
// better for most cases.
|
||||||
type ScrollBar struct {
|
type ScrollBar struct {
|
||||||
*core.Core
|
entity tomo.ContainerEntity
|
||||||
core core.CoreControl
|
|
||||||
|
|
||||||
vertical bool
|
vertical bool
|
||||||
enabled bool
|
enabled bool
|
||||||
@@ -38,28 +44,42 @@ type ScrollBar struct {
|
|||||||
onScroll func (viewport image.Point)
|
onScroll func (viewport image.Point)
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewScrollBar creates a new scroll bar. If vertical is set to true, the scroll
|
// NewScrollBar creates a new scroll bar.
|
||||||
// bar will be vertical instead of horizontal.
|
func NewScrollBar (orientation Orientation) (element *ScrollBar) {
|
||||||
func NewScrollBar (vertical bool) (element *ScrollBar) {
|
|
||||||
element = &ScrollBar {
|
element = &ScrollBar {
|
||||||
vertical: vertical,
|
vertical: bool(orientation),
|
||||||
enabled: true,
|
enabled: true,
|
||||||
}
|
}
|
||||||
if vertical {
|
if orientation == Vertical {
|
||||||
element.theme.Case = tomo.C("tomo", "scrollBarHorizontal")
|
element.theme.Case = tomo.C("tomo", "scrollBarHorizontal")
|
||||||
} else {
|
} else {
|
||||||
element.theme.Case = tomo.C("tomo", "scrollBarVertical")
|
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()
|
element.updateMinimumSize()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollBar) handleResize () {
|
// Entity returns this element's entity.
|
||||||
if element.core.HasImage() {
|
func (element *ScrollBar) Entity () tomo.Entity {
|
||||||
element.recalculate()
|
return element.entity
|
||||||
element.draw()
|
}
|
||||||
|
|
||||||
|
// 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) {
|
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) {
|
if point.In(element.bar) {
|
||||||
// the mouse is pressed down within the bar's handle
|
// the mouse is pressed down within the bar's handle
|
||||||
element.dragging = true
|
element.dragging = true
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
element.dragOffset =
|
element.dragOffset =
|
||||||
point.Sub(element.bar.Min).
|
point.Sub(element.bar.Min).
|
||||||
Add(element.Bounds().Min)
|
Add(element.entity.Bounds().Min)
|
||||||
element.dragTo(point)
|
element.dragTo(point)
|
||||||
} else {
|
} else {
|
||||||
// the mouse is pressed down within the bar's gutter
|
// 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) {
|
func (element *ScrollBar) HandleMouseUp (x, y int, button input.Button) {
|
||||||
if element.dragging {
|
if element.dragging {
|
||||||
element.dragging = false
|
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) {
|
func (element *ScrollBar) SetEnabled (enabled bool) {
|
||||||
if element.enabled == enabled { return }
|
if element.enabled == enabled { return }
|
||||||
element.enabled = enabled
|
element.enabled = enabled
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Enabled returns whether or not the element is enabled.
|
// 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) {
|
func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) {
|
||||||
element.contentBounds = content
|
element.contentBounds = content
|
||||||
element.viewportBounds = viewport
|
element.viewportBounds = viewport
|
||||||
element.recalculate()
|
element.entity.Invalidate()
|
||||||
element.drawAndPush()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnScroll sets a function to be called when the user tries to move the scroll
|
// 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) {
|
func (element *ScrollBar) SetTheme (new tomo.Theme) {
|
||||||
if new == element.theme.Theme { return }
|
if new == element.theme.Theme { return }
|
||||||
element.theme.Theme = new
|
element.theme.Theme = new
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
@@ -171,7 +190,7 @@ func (element *ScrollBar) SetConfig (new tomo.Config) {
|
|||||||
if new == element.config.Config { return }
|
if new == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.drawAndPush()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollBar) isAfterHandle (point image.Point) bool {
|
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 {
|
func (element *ScrollBar) fallbackDragOffset () image.Point {
|
||||||
if element.vertical {
|
if element.vertical {
|
||||||
return element.Bounds().Min.
|
return element.entity.Bounds().Min.
|
||||||
Add(image.Pt(0, element.bar.Dy() / 2))
|
Add(image.Pt(0, element.bar.Dy() / 2))
|
||||||
} else {
|
} else {
|
||||||
return element.Bounds().Min.
|
return element.entity.Bounds().Min.
|
||||||
Add(image.Pt(element.bar.Dx() / 2, 0))
|
Add(image.Pt(element.bar.Dx() / 2, 0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +255,7 @@ func (element *ScrollBar) recalculate () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollBar) recalculateVertical () {
|
func (element *ScrollBar) recalculateVertical () {
|
||||||
bounds := element.Bounds()
|
bounds := element.entity.Bounds()
|
||||||
padding := element.theme.Padding(tomo.PatternGutter)
|
padding := element.theme.Padding(tomo.PatternGutter)
|
||||||
element.track = padding.Apply(bounds)
|
element.track = padding.Apply(bounds)
|
||||||
|
|
||||||
@@ -263,7 +282,7 @@ func (element *ScrollBar) recalculateVertical () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *ScrollBar) recalculateHorizontal () {
|
func (element *ScrollBar) recalculateHorizontal () {
|
||||||
bounds := element.Bounds()
|
bounds := element.entity.Bounds()
|
||||||
padding := element.theme.Padding(tomo.PatternGutter)
|
padding := element.theme.Padding(tomo.PatternGutter)
|
||||||
element.track = padding.Apply(bounds)
|
element.track = padding.Apply(bounds)
|
||||||
|
|
||||||
@@ -293,33 +312,12 @@ func (element *ScrollBar) updateMinimumSize () {
|
|||||||
gutterPadding := element.theme.Padding(tomo.PatternGutter)
|
gutterPadding := element.theme.Padding(tomo.PatternGutter)
|
||||||
handlePadding := element.theme.Padding(tomo.PatternHandle)
|
handlePadding := element.theme.Padding(tomo.PatternHandle)
|
||||||
if element.vertical {
|
if element.vertical {
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
gutterPadding.Horizontal() + handlePadding.Horizontal(),
|
gutterPadding.Horizontal() + handlePadding.Horizontal(),
|
||||||
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
||||||
} else {
|
} else {
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
||||||
gutterPadding.Vertical() + handlePadding.Vertical())
|
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 "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
// Slider is a slider control with a floating point value between zero and one.
|
// Slider is a slider control with a floating point value between zero and one.
|
||||||
type Slider struct {
|
type Slider struct {
|
||||||
*core.Core
|
entity tomo.FocusableEntity
|
||||||
*core.FocusableCore
|
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
|
|
||||||
value float64
|
value float64
|
||||||
vertical bool
|
vertical bool
|
||||||
dragging bool
|
dragging bool
|
||||||
|
enabled bool
|
||||||
dragOffset int
|
dragOffset int
|
||||||
track image.Rectangle
|
track image.Rectangle
|
||||||
bar image.Rectangle
|
bar image.Rectangle
|
||||||
|
|
||||||
config config.Wrapped
|
config config.Wrapped
|
||||||
theme theme.Wrapped
|
theme theme.Wrapped
|
||||||
@@ -28,25 +26,77 @@ type Slider struct {
|
|||||||
onRelease func ()
|
onRelease func ()
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSlider creates a new slider with the specified value. If vertical is set
|
// NewSlider creates a new slider with the specified value.
|
||||||
// to true,
|
func NewSlider (value float64, orientation Orientation) (element *Slider) {
|
||||||
func NewSlider (value float64, vertical bool) (element *Slider) {
|
|
||||||
element = &Slider {
|
element = &Slider {
|
||||||
value: value,
|
value: value,
|
||||||
vertical: vertical,
|
vertical: bool(orientation),
|
||||||
}
|
}
|
||||||
if vertical {
|
if orientation == Vertical {
|
||||||
element.theme.Case = tomo.C("tomo", "sliderVertical")
|
element.theme.Case = tomo.C("tomo", "sliderVertical")
|
||||||
} else {
|
} else {
|
||||||
element.theme.Case = tomo.C("tomo", "sliderHorizontal")
|
element.theme.Case = tomo.C("tomo", "sliderHorizontal")
|
||||||
}
|
}
|
||||||
element.Core, element.core = core.NewCore(element, element.draw)
|
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
|
||||||
element.FocusableCore,
|
|
||||||
element.focusableControl = core.NewFocusableCore(element.core, element.redo)
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
return
|
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) {
|
func (element *Slider) HandleMouseDown (x, y int, button input.Button) {
|
||||||
if !element.Enabled() { return }
|
if !element.Enabled() { return }
|
||||||
element.Focus()
|
element.Focus()
|
||||||
@@ -56,7 +106,7 @@ func (element *Slider) HandleMouseDown (x, y int, button input.Button) {
|
|||||||
if element.onSlide != nil {
|
if element.onSlide != nil {
|
||||||
element.onSlide()
|
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 {
|
if element.onRelease != nil {
|
||||||
element.onRelease()
|
element.onRelease()
|
||||||
}
|
}
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Slider) HandleMotion (x, y int) {
|
func (element *Slider) HandleMotion (x, y int) {
|
||||||
@@ -76,7 +126,7 @@ func (element *Slider) HandleMotion (x, y int) {
|
|||||||
if element.onSlide != nil {
|
if element.onSlide != nil {
|
||||||
element.onSlide()
|
element.onSlide()
|
||||||
}
|
}
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,11 +160,6 @@ func (element *Slider) Value () (value float64) {
|
|||||||
return element.value
|
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.
|
// SetValue sets the slider's value.
|
||||||
func (element *Slider) SetValue (value float64) {
|
func (element *Slider) SetValue (value float64) {
|
||||||
if value < 0 { value = 0 }
|
if value < 0 { value = 0 }
|
||||||
@@ -126,7 +171,7 @@ func (element *Slider) SetValue (value float64) {
|
|||||||
if element.onRelease != nil {
|
if element.onRelease != nil {
|
||||||
element.onRelease()
|
element.onRelease()
|
||||||
}
|
}
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// OnSlide sets a function to be called every time the slider handle changes
|
// 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) {
|
func (element *Slider) SetTheme (new tomo.Theme) {
|
||||||
if new == element.theme.Theme { return }
|
if new == element.theme.Theme { return }
|
||||||
element.theme.Theme = new
|
element.theme.Theme = new
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
@@ -152,7 +197,7 @@ func (element *Slider) SetConfig (new tomo.Config) {
|
|||||||
if new == element.config.Config { return }
|
if new == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Slider) changeValue (delta float64) {
|
func (element *Slider) changeValue (delta float64) {
|
||||||
@@ -166,7 +211,7 @@ func (element *Slider) changeValue (delta float64) {
|
|||||||
if element.onRelease != nil {
|
if element.onRelease != nil {
|
||||||
element.onRelease()
|
element.onRelease()
|
||||||
}
|
}
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Slider) valueFor (x, y int) (value float64) {
|
func (element *Slider) valueFor (x, y int) (value float64) {
|
||||||
@@ -190,51 +235,12 @@ func (element *Slider) updateMinimumSize () {
|
|||||||
gutterPadding := element.theme.Padding(tomo.PatternGutter)
|
gutterPadding := element.theme.Padding(tomo.PatternGutter)
|
||||||
handlePadding := element.theme.Padding(tomo.PatternHandle)
|
handlePadding := element.theme.Padding(tomo.PatternHandle)
|
||||||
if element.vertical {
|
if element.vertical {
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
gutterPadding.Horizontal() + handlePadding.Horizontal(),
|
gutterPadding.Horizontal() + handlePadding.Horizontal(),
|
||||||
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
gutterPadding.Vertical() + handlePadding.Vertical() * 2)
|
||||||
} else {
|
} else {
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
gutterPadding.Horizontal() + handlePadding.Horizontal() * 2,
|
||||||
gutterPadding.Vertical() + handlePadding.Vertical())
|
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
|
package elements
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
// Spacer can be used to put space between two elements..
|
// Spacer can be used to put space between two elements..
|
||||||
type Spacer struct {
|
type Spacer struct {
|
||||||
*core.Core
|
entity tomo.Entity
|
||||||
core core.CoreControl
|
|
||||||
line bool
|
line bool
|
||||||
|
|
||||||
config config.Wrapped
|
config config.Wrapped
|
||||||
theme theme.Wrapped
|
theme theme.Wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSpacer creates a new spacer. If line is set to true, the spacer will be
|
// NewSpacer creates a new spacer.
|
||||||
// filled with a line color, and if compressed to its minimum width or height,
|
func NewSpacer () (element *Spacer) {
|
||||||
// will appear as a line.
|
element = &Spacer { }
|
||||||
func NewSpacer (line bool) (element *Spacer) {
|
element.entity = tomo.NewEntity(element)
|
||||||
element = &Spacer { line: line }
|
|
||||||
element.theme.Case = tomo.C("tomo", "spacer")
|
element.theme.Case = tomo.C("tomo", "spacer")
|
||||||
element.Core, element.core = core.NewCore(element, element.draw)
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
return
|
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.
|
/// SetLine sets whether or not the spacer will appear as a colored line.
|
||||||
func (element *Spacer) SetLine (line bool) {
|
func (element *Spacer) SetLine (line bool) {
|
||||||
if element.line == line { return }
|
if element.line == line { return }
|
||||||
element.line = line
|
element.line = line
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
if element.core.HasImage() {
|
element.entity.Invalidate()
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetTheme sets the element's theme.
|
// SetTheme sets the element's theme.
|
||||||
func (element *Spacer) SetTheme (new tomo.Theme) {
|
func (element *Spacer) SetTheme (new tomo.Theme) {
|
||||||
if new == element.theme.Theme { return }
|
if new == element.theme.Theme { return }
|
||||||
element.theme.Theme = new
|
element.theme.Theme = new
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
func (element *Spacer) SetConfig (new tomo.Config) {
|
func (element *Spacer) SetConfig (new tomo.Config) {
|
||||||
if new == element.config.Config { return }
|
if new == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Spacer) updateMinimumSize () {
|
func (element *Spacer) updateMinimumSize () {
|
||||||
if element.line {
|
if element.line {
|
||||||
padding := element.theme.Padding(tomo.PatternLine)
|
padding := element.theme.Padding(tomo.PatternLine)
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
padding.Horizontal(),
|
padding.Horizontal(),
|
||||||
padding.Vertical())
|
padding.Vertical())
|
||||||
} else {
|
} else {
|
||||||
element.core.SetMinimumSize(1, 1)
|
element.entity.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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,20 +3,18 @@ package elements
|
|||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
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/textdraw"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/default/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
// Switch is a toggle-able on/off switch with an optional label. It is
|
// Switch is a toggle-able on/off switch with an optional label. It is
|
||||||
// functionally identical to Checkbox, but plays a different semantic role.
|
// functionally identical to Checkbox, but plays a different semantic role.
|
||||||
type Switch struct {
|
type Switch struct {
|
||||||
*core.Core
|
entity tomo.FocusableEntity
|
||||||
*core.FocusableCore
|
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
drawer textdraw.Drawer
|
drawer textdraw.Drawer
|
||||||
|
|
||||||
|
enabled bool
|
||||||
pressed bool
|
pressed bool
|
||||||
checked bool
|
checked bool
|
||||||
text string
|
text string
|
||||||
@@ -31,12 +29,11 @@ type Switch struct {
|
|||||||
func NewSwitch (text string, on bool) (element *Switch) {
|
func NewSwitch (text string, on bool) (element *Switch) {
|
||||||
element = &Switch {
|
element = &Switch {
|
||||||
checked: on,
|
checked: on,
|
||||||
text: text,
|
text: text,
|
||||||
|
enabled: true,
|
||||||
}
|
}
|
||||||
|
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
|
||||||
element.theme.Case = tomo.C("tomo", "switch")
|
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 (
|
element.drawer.SetFace (element.theme.FontFace (
|
||||||
tomo.FontStyleRegular,
|
tomo.FontStyleRegular,
|
||||||
tomo.FontSizeNormal))
|
tomo.FontSizeNormal))
|
||||||
@@ -45,129 +42,25 @@ func NewSwitch (text string, on bool) (element *Switch) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Switch) HandleMouseDown (x, y int, button input.Button) {
|
// Entity returns this element's entity.
|
||||||
if !element.Enabled() { return }
|
func (element *Switch) Entity () tomo.Entity {
|
||||||
element.Focus()
|
return element.entity
|
||||||
element.pressed = true
|
|
||||||
element.redo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Switch) HandleMouseUp (x, y int, button input.Button) {
|
// Draw causes the element to draw to the specified destination canvas.
|
||||||
if button != input.ButtonLeft || !element.pressed { return }
|
func (element *Switch) Draw (destination canvas.Canvas) {
|
||||||
|
bounds := element.entity.Bounds()
|
||||||
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()
|
|
||||||
handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min)
|
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)
|
gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min)
|
||||||
|
|
||||||
state := tomo.State {
|
state := tomo.State {
|
||||||
Disabled: !element.Enabled(),
|
Disabled: !element.Enabled(),
|
||||||
Focused: element.Focused(),
|
Focused: element.entity.Focused(),
|
||||||
Pressed: element.pressed,
|
Pressed: element.pressed,
|
||||||
|
On: element.checked,
|
||||||
}
|
}
|
||||||
|
|
||||||
element.core.DrawBackground (
|
element.entity.DrawBackground(destination)
|
||||||
element.theme.Pattern(tomo.PatternBackground, state))
|
|
||||||
|
|
||||||
if element.checked {
|
if element.checked {
|
||||||
handleBounds.Min.X += bounds.Dy()
|
handleBounds.Min.X += bounds.Dy()
|
||||||
@@ -185,11 +78,11 @@ func (element *Switch) draw () {
|
|||||||
|
|
||||||
gutterPattern := element.theme.Pattern (
|
gutterPattern := element.theme.Pattern (
|
||||||
tomo.PatternGutter, state)
|
tomo.PatternGutter, state)
|
||||||
gutterPattern.Draw(element.core, gutterBounds)
|
gutterPattern.Draw(destination, gutterBounds)
|
||||||
|
|
||||||
handlePattern := element.theme.Pattern (
|
handlePattern := element.theme.Pattern (
|
||||||
tomo.PatternHandle, state)
|
tomo.PatternHandle, state)
|
||||||
handlePattern.Draw(element.core, handleBounds)
|
handlePattern.Draw(destination, handleBounds)
|
||||||
|
|
||||||
textBounds := element.drawer.LayoutBounds()
|
textBounds := element.drawer.LayoutBounds()
|
||||||
offset := bounds.Min.Add(image.Point {
|
offset := bounds.Min.Add(image.Point {
|
||||||
@@ -201,5 +94,121 @@ func (element *Switch) draw () {
|
|||||||
offset.X -= textBounds.Min.X
|
offset.X -= textBounds.Min.X
|
||||||
|
|
||||||
foreground := element.theme.Color(tomo.ColorForeground, state)
|
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 "time"
|
||||||
import "image"
|
import "image"
|
||||||
import "image/color"
|
import "image/color"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
import "git.tebibyte.media/sashakoshka/tomo/shatter"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
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/shapes"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
|
import "git.tebibyte.media/sashakoshka/tomo/artist/patterns"
|
||||||
import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font"
|
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
|
// Artist is an element that displays shapes and patterns drawn by the artist
|
||||||
// package in order to test it.
|
// package in order to test it.
|
||||||
type Artist struct {
|
type Artist struct {
|
||||||
*core.Core
|
entity tomo.Entity
|
||||||
core core.CoreControl
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewArtist creates a new artist test element.
|
// NewArtist creates a new artist test element.
|
||||||
func NewArtist () (element *Artist) {
|
func NewArtist () (element *Artist) {
|
||||||
element = &Artist { }
|
element = &Artist { }
|
||||||
element.Core, element.core = core.NewCore(element, element.draw)
|
element.entity = tomo.NewEntity(element)
|
||||||
element.core.SetMinimumSize(240, 240)
|
element.entity.SetMinimumSize(240, 240)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Artist) draw () {
|
func (element *Artist) Entity () tomo.Entity {
|
||||||
bounds := element.Bounds()
|
return element.entity
|
||||||
patterns.Uhex(0x000000FF).Draw(element.core, bounds)
|
}
|
||||||
|
|
||||||
|
func (element *Artist) Draw (destination canvas.Canvas) {
|
||||||
|
bounds := element.entity.Bounds()
|
||||||
|
patterns.Uhex(0x000000FF).Draw(destination, bounds)
|
||||||
|
|
||||||
drawStart := time.Now()
|
drawStart := time.Now()
|
||||||
|
|
||||||
// 0, 0 - 3, 0
|
// 0, 0 - 3, 0
|
||||||
for x := 0; x < 4; x ++ {
|
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
|
// 4, 0
|
||||||
c40 := element.cellAt(4, 0)
|
c40 := element.cellAt(destination, 4, 0)
|
||||||
shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
|
shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1)
|
||||||
shapes.ColorLine (
|
shapes.ColorLine (
|
||||||
c40, artist.Hex(0xFF0000FF), 1,
|
c40, artist.Hex(0xFF0000FF), 1,
|
||||||
c40.Bounds().Min, c40.Bounds().Max)
|
c40.Bounds().Min, c40.Bounds().Max)
|
||||||
|
|
||||||
// 0, 1
|
// 0, 1
|
||||||
c01 := element.cellAt(0, 1)
|
c01 := element.cellAt(destination, 0, 1)
|
||||||
shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 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
|
// 1, 1 - 3, 1
|
||||||
for x := 1; x < 4; x ++ {
|
for x := 1; x < 4; x ++ {
|
||||||
c := element.cellAt(x, 1)
|
c := element.cellAt(destination, x, 1)
|
||||||
shapes.StrokeColorRectangle (
|
shapes.StrokeColorRectangle (
|
||||||
element.core, artist.Hex(0x888888FF),
|
destination, artist.Hex(0x888888FF),
|
||||||
c.Bounds(), 1)
|
c.Bounds(), 1)
|
||||||
shapes.StrokeColorEllipse (
|
shapes.StrokeColorEllipse (
|
||||||
element.core,
|
destination,
|
||||||
[]color.RGBA {
|
[]color.RGBA {
|
||||||
artist.Hex(0xFF0000FF),
|
artist.Hex(0xFF0000FF),
|
||||||
artist.Hex(0x00FF00FF),
|
artist.Hex(0x00FF00FF),
|
||||||
@@ -68,7 +71,7 @@ func (element *Artist) draw () {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4, 1
|
// 4, 1
|
||||||
c41 := element.cellAt(4, 1)
|
c41 := element.cellAt(destination, 4, 1)
|
||||||
shatterPos := c41.Bounds().Min
|
shatterPos := c41.Bounds().Min
|
||||||
rocks := []image.Rectangle {
|
rocks := []image.Rectangle {
|
||||||
image.Rect(3, 12, 13, 23).Add(shatterPos),
|
image.Rect(3, 12, 13, 23).Add(shatterPos),
|
||||||
@@ -85,46 +88,46 @@ func (element *Artist) draw () {
|
|||||||
patterns.Uhex(0xFF00FFFF),
|
patterns.Uhex(0xFF00FFFF),
|
||||||
patterns.Uhex(0xFFFF00FF),
|
patterns.Uhex(0xFFFF00FF),
|
||||||
patterns.Uhex(0x00FFFFFF),
|
patterns.Uhex(0x00FFFFFF),
|
||||||
} [index % 5].Draw(element.core, tile)
|
} [index % 5].Draw(destination, tile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 0, 2
|
// 0, 2
|
||||||
c02 := element.cellAt(0, 2)
|
c02 := element.cellAt(destination, 0, 2)
|
||||||
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
|
shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1)
|
||||||
shapes.FillEllipse(c02, c41, c02.Bounds())
|
shapes.FillEllipse(c02, c41, c02.Bounds())
|
||||||
|
|
||||||
// 1, 2
|
// 1, 2
|
||||||
c12 := element.cellAt(1, 2)
|
c12 := element.cellAt(destination, 1, 2)
|
||||||
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
|
shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1)
|
||||||
shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
|
shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5)
|
||||||
|
|
||||||
// 2, 2
|
// 2, 2
|
||||||
c22 := element.cellAt(2, 2)
|
c22 := element.cellAt(destination, 2, 2)
|
||||||
shapes.FillRectangle(c22, c41, c22.Bounds())
|
shapes.FillRectangle(c22, c41, c22.Bounds())
|
||||||
|
|
||||||
// 3, 2
|
// 3, 2
|
||||||
c32 := element.cellAt(3, 2)
|
c32 := element.cellAt(destination, 3, 2)
|
||||||
shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5)
|
shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5)
|
||||||
|
|
||||||
// 4, 2
|
// 4, 2
|
||||||
c42 := element.cellAt(4, 2)
|
c42 := element.cellAt(destination, 4, 2)
|
||||||
|
|
||||||
// 0, 3
|
// 0, 3
|
||||||
c03 := element.cellAt(0, 3)
|
c03 := element.cellAt(destination, 0, 3)
|
||||||
patterns.Border {
|
patterns.Border {
|
||||||
Canvas: element.thingy(c42),
|
Canvas: element.thingy(c42),
|
||||||
Inset: artist.Inset { 8, 8, 8, 8 },
|
Inset: artist.Inset { 8, 8, 8, 8 },
|
||||||
}.Draw(c03, c03.Bounds())
|
}.Draw(c03, c03.Bounds())
|
||||||
|
|
||||||
// 1, 3
|
// 1, 3
|
||||||
c13 := element.cellAt(1, 3)
|
c13 := element.cellAt(destination, 1, 3)
|
||||||
patterns.Border {
|
patterns.Border {
|
||||||
Canvas: element.thingy(c42),
|
Canvas: element.thingy(c42),
|
||||||
Inset: artist.Inset { 8, 8, 8, 8 },
|
Inset: artist.Inset { 8, 8, 8, 8 },
|
||||||
}.Draw(c13, c13.Bounds().Inset(10))
|
}.Draw(c13, c13.Bounds().Inset(10))
|
||||||
|
|
||||||
// 2, 3
|
// 2, 3
|
||||||
c23 := element.cellAt(2, 3)
|
c23 := element.cellAt(destination, 2, 3)
|
||||||
patterns.Border {
|
patterns.Border {
|
||||||
Canvas: element.thingy(c42),
|
Canvas: element.thingy(c42),
|
||||||
Inset: artist.Inset { 8, 8, 8, 8 },
|
Inset: artist.Inset { 8, 8, 8, 8 },
|
||||||
@@ -143,51 +146,51 @@ func (element *Artist) draw () {
|
|||||||
drawTime.Milliseconds(),
|
drawTime.Milliseconds(),
|
||||||
drawTime.Microseconds())))
|
drawTime.Microseconds())))
|
||||||
textDrawer.Draw (
|
textDrawer.Draw (
|
||||||
element.core, artist.Hex(0xFFFFFFFF),
|
destination, artist.Hex(0xFFFFFFFF),
|
||||||
image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24))
|
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)
|
bounds = bounds.Inset(4)
|
||||||
c := artist.Hex(0xFFFFFFFF)
|
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 (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
destination, c, weight,
|
||||||
image.Pt(bounds.Max.X, bounds.Min.Y),
|
image.Pt(bounds.Max.X, bounds.Min.Y),
|
||||||
image.Pt(bounds.Min.X, bounds.Max.Y))
|
image.Pt(bounds.Min.X, bounds.Max.Y))
|
||||||
shapes.ColorLine (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
destination, c, weight,
|
||||||
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
|
image.Pt(bounds.Max.X, bounds.Min.Y + 16),
|
||||||
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
|
image.Pt(bounds.Min.X, bounds.Max.Y - 16))
|
||||||
shapes.ColorLine (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
destination, c, weight,
|
||||||
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
|
image.Pt(bounds.Min.X, bounds.Min.Y + 16),
|
||||||
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
|
image.Pt(bounds.Max.X, bounds.Max.Y - 16))
|
||||||
shapes.ColorLine (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
destination, c, weight,
|
||||||
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
|
image.Pt(bounds.Min.X + 20, bounds.Min.Y),
|
||||||
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
|
image.Pt(bounds.Max.X - 20, bounds.Max.Y))
|
||||||
shapes.ColorLine (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
destination, c, weight,
|
||||||
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
|
image.Pt(bounds.Max.X - 20, bounds.Min.Y),
|
||||||
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
|
image.Pt(bounds.Min.X + 20, bounds.Max.Y))
|
||||||
shapes.ColorLine (
|
shapes.ColorLine (
|
||||||
element.core, c, weight,
|
destination, c, weight,
|
||||||
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
|
image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2),
|
||||||
image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2))
|
image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2))
|
||||||
shapes.ColorLine (
|
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.Min.Y),
|
||||||
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
|
image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Artist) cellAt (x, y int) (canvas.Canvas) {
|
func (element *Artist) cellAt (destination canvas.Canvas, x, y int) (canvas.Canvas) {
|
||||||
bounds := element.Bounds()
|
bounds := element.entity.Bounds()
|
||||||
cellBounds := image.Rectangle { }
|
cellBounds := image.Rectangle { }
|
||||||
cellBounds.Min = bounds.Min
|
cellBounds.Min = bounds.Min
|
||||||
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
|
cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5
|
||||||
cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4
|
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(),
|
x * cellBounds.Dx(),
|
||||||
y * cellBounds.Dy())))
|
y * cellBounds.Dy())))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,17 +4,16 @@ import "image"
|
|||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
import "git.tebibyte.media/sashakoshka/tomo/input"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist"
|
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/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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
// Mouse is an element capable of testing mouse input. When the mouse is clicked
|
// Mouse is an element capable of testing mouse input. When the mouse is clicked
|
||||||
// and dragged on it, it draws a trail.
|
// and dragged on it, it draws a trail.
|
||||||
type Mouse struct {
|
type Mouse struct {
|
||||||
*core.Core
|
entity tomo.Entity
|
||||||
core core.CoreControl
|
pressed bool
|
||||||
drawing bool
|
|
||||||
lastMousePos image.Point
|
lastMousePos image.Point
|
||||||
|
|
||||||
config config.Wrapped
|
config config.Wrapped
|
||||||
@@ -24,69 +23,63 @@ type Mouse struct {
|
|||||||
// NewMouse creates a new mouse test element.
|
// NewMouse creates a new mouse test element.
|
||||||
func NewMouse () (element *Mouse) {
|
func NewMouse () (element *Mouse) {
|
||||||
element = &Mouse { }
|
element = &Mouse { }
|
||||||
element.theme.Case = tomo.C("tomo", "piano")
|
element.theme.Case = tomo.C("tomo", "mouse")
|
||||||
element.Core, element.core = core.NewCore(element, element.draw)
|
element.entity = tomo.NewEntity(element)
|
||||||
element.core.SetMinimumSize(32, 32)
|
element.entity.SetMinimumSize(32, 32)
|
||||||
return
|
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.
|
// SetTheme sets the element's theme.
|
||||||
func (element *Mouse) SetTheme (new tomo.Theme) {
|
func (element *Mouse) SetTheme (new tomo.Theme) {
|
||||||
element.theme.Theme = new
|
element.theme.Theme = new
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
func (element *Mouse) SetConfig (new tomo.Config) {
|
func (element *Mouse) SetConfig (new tomo.Config) {
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
|
||||||
|
|
||||||
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)))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Mouse) HandleMouseDown (x, y int, button input.Button) {
|
func (element *Mouse) HandleMouseDown (x, y int, button input.Button) {
|
||||||
element.drawing = true
|
element.pressed = true
|
||||||
element.lastMousePos = image.Pt(x, y)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
|
func (element *Mouse) HandleMouseUp (x, y int, button input.Button) {
|
||||||
element.drawing = false
|
element.pressed = false
|
||||||
mousePos := image.Pt(x, y)
|
|
||||||
element.core.DamageRegion (shapes.ColorLine (
|
|
||||||
element.core, artist.Hex(0x000000FF), 1,
|
|
||||||
element.lastMousePos, mousePos))
|
|
||||||
element.lastMousePos = mousePos
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Mouse) HandleMotion (x, y int) {
|
func (element *Mouse) HandleMotion (x, y int) {
|
||||||
if !element.drawing { return }
|
if !element.pressed { return }
|
||||||
mousePos := image.Pt(x, y)
|
element.lastMousePos = image.Pt(x, y)
|
||||||
element.core.DamageRegion (shapes.ColorLine (
|
element.entity.Invalidate()
|
||||||
element.core, artist.Hex(0x000000FF), 1,
|
|
||||||
element.lastMousePos, mousePos))
|
|
||||||
element.lastMousePos = mousePos
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,20 @@ import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
|||||||
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
|
import "git.tebibyte.media/sashakoshka/tomo/textmanip"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
|
import "git.tebibyte.media/sashakoshka/tomo/fixedutil"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
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/theme"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
|
type textBoxEntity interface {
|
||||||
|
tomo.FocusableEntity
|
||||||
|
tomo.ScrollableEntity
|
||||||
|
tomo.LayoutEntity
|
||||||
|
}
|
||||||
|
|
||||||
// TextBox is a single-line text input.
|
// TextBox is a single-line text input.
|
||||||
type TextBox struct {
|
type TextBox struct {
|
||||||
*core.Core
|
entity textBoxEntity
|
||||||
*core.FocusableCore
|
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
|
|
||||||
|
enabled bool
|
||||||
lastClick time.Time
|
lastClick time.Time
|
||||||
dragging int
|
dragging int
|
||||||
dot textmanip.Dot
|
dot textmanip.Dot
|
||||||
@@ -46,16 +49,9 @@ type TextBox struct {
|
|||||||
// a value. When the value is empty, the placeholder will be displayed in gray
|
// a value. When the value is empty, the placeholder will be displayed in gray
|
||||||
// text.
|
// text.
|
||||||
func NewTextBox (placeholder, value string) (element *TextBox) {
|
func NewTextBox (placeholder, value string) (element *TextBox) {
|
||||||
element = &TextBox { }
|
element = &TextBox { enabled: true }
|
||||||
element.theme.Case = tomo.C("tomo", "textBox")
|
element.theme.Case = tomo.C("tomo", "textBox")
|
||||||
element.Core, element.core = core.NewCore(element, element.handleResize)
|
element.entity = tomo.NewEntity(element).(textBoxEntity)
|
||||||
element.FocusableCore,
|
|
||||||
element.focusableControl = core.NewFocusableCore (element.core, func () {
|
|
||||||
if element.core.HasImage () {
|
|
||||||
element.draw()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
element.placeholder = placeholder
|
element.placeholder = placeholder
|
||||||
element.placeholderDrawer.SetFace (element.theme.FontFace (
|
element.placeholderDrawer.SetFace (element.theme.FontFace (
|
||||||
tomo.FontStyleRegular,
|
tomo.FontStyleRegular,
|
||||||
@@ -69,17 +65,87 @@ func NewTextBox (placeholder, value string) (element *TextBox) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) handleResize () {
|
// Entity returns this element's entity.
|
||||||
element.scrollToCursor()
|
func (element *TextBox) Entity () tomo.Entity {
|
||||||
element.draw()
|
return element.entity
|
||||||
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
|
}
|
||||||
parent.NotifyScrollBoundsChange(element)
|
|
||||||
|
// 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) {
|
func (element *TextBox) HandleMouseDown (x, y int, button input.Button) {
|
||||||
if !element.Enabled() { return }
|
if !element.Enabled() { return }
|
||||||
if !element.Focused() { element.Focus() }
|
element.Focus()
|
||||||
|
|
||||||
if button == input.ButtonLeft {
|
if button == input.ButtonLeft {
|
||||||
runeIndex := element.atPosition(image.Pt(x, y))
|
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.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))
|
runeIndex := element.atPosition(image.Pt(x, y))
|
||||||
if runeIndex > -1 {
|
if runeIndex > -1 {
|
||||||
element.dot.End = runeIndex
|
element.dot.End = runeIndex
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
case 2:
|
case 2:
|
||||||
@@ -125,14 +191,14 @@ func (element *TextBox) HandleMotion (x, y int) {
|
|||||||
element.text,
|
element.text,
|
||||||
runeIndex)
|
runeIndex)
|
||||||
}
|
}
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) textOffset () image.Point {
|
func (element *TextBox) textOffset () image.Point {
|
||||||
padding := element.theme.Padding(tomo.PatternInput)
|
padding := element.theme.Padding(tomo.PatternInput)
|
||||||
bounds := element.Bounds()
|
bounds := element.entity.Bounds()
|
||||||
innerBounds := padding.Apply(bounds)
|
innerBounds := padding.Apply(bounds)
|
||||||
textHeight := element.valueDrawer.LineHeight().Round()
|
textHeight := element.valueDrawer.LineHeight().Round()
|
||||||
return bounds.Min.Add (image.Pt (
|
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))
|
element.clipboardPut(element.dot.Slice(element.text))
|
||||||
|
|
||||||
case key == 'v' && modifiers.Control:
|
case key == 'v' && modifiers.Control:
|
||||||
window := element.core.Window()
|
window := element.entity.Window()
|
||||||
if window == nil { break }
|
if window == nil { break }
|
||||||
window.Paste (func (d data.Data, err error) {
|
window.Paste (func (d data.Data, err error) {
|
||||||
if err != nil { return }
|
if err != nil { return }
|
||||||
@@ -262,25 +328,17 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (textChanged || scrollMemory != element.scroll) {
|
if (textChanged || scrollMemory != element.scroll) {
|
||||||
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
|
element.entity.NotifyScrollBoundsChange()
|
||||||
parent.NotifyScrollBoundsChange(element)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if altered {
|
if altered {
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (element *TextBox) clipboardPut (text []rune) {
|
|
||||||
window := element.core.Window()
|
|
||||||
if window != nil {
|
|
||||||
window.Copy(data.Bytes(data.MimePlain, []byte(string(text))))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { }
|
||||||
|
|
||||||
|
// SetPlaceholder sets the element's placeholder text.
|
||||||
func (element *TextBox) SetPlaceholder (placeholder string) {
|
func (element *TextBox) SetPlaceholder (placeholder string) {
|
||||||
if element.placeholder == placeholder { return }
|
if element.placeholder == placeholder { return }
|
||||||
|
|
||||||
@@ -288,9 +346,10 @@ func (element *TextBox) SetPlaceholder (placeholder string) {
|
|||||||
element.placeholderDrawer.SetText([]rune(placeholder))
|
element.placeholderDrawer.SetText([]rune(placeholder))
|
||||||
|
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetValue sets the input's value.
|
||||||
func (element *TextBox) SetValue (text string) {
|
func (element *TextBox) SetValue (text string) {
|
||||||
// if element.text == text { return }
|
// if element.text == text { return }
|
||||||
|
|
||||||
@@ -301,27 +360,35 @@ func (element *TextBox) SetValue (text string) {
|
|||||||
element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
|
element.dot = textmanip.EmptyDot(element.valueDrawer.Length())
|
||||||
}
|
}
|
||||||
element.scrollToCursor()
|
element.scrollToCursor()
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Value returns the input's value.
|
||||||
func (element *TextBox) Value () (value string) {
|
func (element *TextBox) Value () (value string) {
|
||||||
return string(element.text)
|
return string(element.text)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Filled returns whether or not this element has a value.
|
||||||
func (element *TextBox) Filled () (filled bool) {
|
func (element *TextBox) Filled () (filled bool) {
|
||||||
return len(element.text) > 0
|
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 (
|
func (element *TextBox) OnKeyDown (
|
||||||
callback func (key input.Key, modifiers input.Modifiers) (handled bool),
|
callback func (key input.Key, modifiers input.Modifiers) (handled bool),
|
||||||
) {
|
) {
|
||||||
element.onKeyDown = callback
|
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 ()) {
|
func (element *TextBox) OnEnter (callback func ()) {
|
||||||
element.onEnter = callback
|
element.onEnter = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnChange specifies a function to be called when the value of this input
|
||||||
|
// changes.
|
||||||
func (element *TextBox) OnChange (callback func ()) {
|
func (element *TextBox) OnChange (callback func ()) {
|
||||||
element.onChange = callback
|
element.onChange = callback
|
||||||
}
|
}
|
||||||
@@ -332,6 +399,23 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) {
|
|||||||
element.onScrollBoundsChange = callback
|
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.
|
// ScrollContentBounds returns the full content size of the element.
|
||||||
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
|
func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) {
|
||||||
bounds = element.valueDrawer.LayoutBounds()
|
bounds = element.valueDrawer.LayoutBounds()
|
||||||
@@ -348,11 +432,6 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) {
|
|||||||
0)
|
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
|
// ScrollTo scrolls the viewport to the specified point relative to
|
||||||
// ScrollBounds.
|
// ScrollBounds.
|
||||||
func (element *TextBox) ScrollTo (position image.Point) {
|
func (element *TextBox) ScrollTo (position image.Point) {
|
||||||
@@ -365,10 +444,8 @@ func (element *TextBox) ScrollTo (position image.Point) {
|
|||||||
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
|
maxPosition := contentBounds.Max.X - element.scrollViewportWidth()
|
||||||
if element.scroll > maxPosition { element.scroll = maxPosition }
|
if element.scroll > maxPosition { element.scroll = maxPosition }
|
||||||
|
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
|
element.entity.NotifyScrollBoundsChange()
|
||||||
parent.NotifyScrollBoundsChange(element)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ScrollAxes returns the supported axes for scrolling.
|
// ScrollAxes returns the supported axes for scrolling.
|
||||||
@@ -376,32 +453,6 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) {
|
|||||||
return true, false
|
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.
|
// SetTheme sets the element's theme.
|
||||||
func (element *TextBox) SetTheme (new tomo.Theme) {
|
func (element *TextBox) SetTheme (new tomo.Theme) {
|
||||||
if new == element.theme.Theme { return }
|
if new == element.theme.Theme { return }
|
||||||
@@ -412,7 +463,7 @@ func (element *TextBox) SetTheme (new tomo.Theme) {
|
|||||||
element.placeholderDrawer.SetFace(face)
|
element.placeholderDrawer.SetFace(face)
|
||||||
element.valueDrawer.SetFace(face)
|
element.valueDrawer.SetFace(face)
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
element.redo()
|
element.entity.Invalidate()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetConfig sets the element's configuration.
|
// SetConfig sets the element's configuration.
|
||||||
@@ -420,13 +471,46 @@ func (element *TextBox) SetConfig (new tomo.Config) {
|
|||||||
if new == element.config.Config { return }
|
if new == element.config.Config { return }
|
||||||
element.config.Config = new
|
element.config.Config = new
|
||||||
element.updateMinimumSize()
|
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 () {
|
func (element *TextBox) updateMinimumSize () {
|
||||||
textBounds := element.placeholderDrawer.LayoutBounds()
|
textBounds := element.placeholderDrawer.LayoutBounds()
|
||||||
padding := element.theme.Padding(tomo.PatternInput)
|
padding := element.theme.Padding(tomo.PatternInput)
|
||||||
element.core.SetMinimumSize (
|
element.entity.SetMinimumSize (
|
||||||
padding.Horizontal() + textBounds.Dx(),
|
padding.Horizontal() + textBounds.Dx(),
|
||||||
padding.Vertical() +
|
padding.Vertical() +
|
||||||
element.placeholderDrawer.LineHeight().Round())
|
element.placeholderDrawer.LineHeight().Round())
|
||||||
@@ -436,81 +520,19 @@ func (element *TextBox) notifyAsyncTextChange () {
|
|||||||
element.runOnChange()
|
element.runOnChange()
|
||||||
element.valueDrawer.SetText(element.text)
|
element.valueDrawer.SetText(element.text)
|
||||||
element.scrollToCursor()
|
element.scrollToCursor()
|
||||||
if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok {
|
element.entity.Invalidate()
|
||||||
parent.NotifyScrollBoundsChange(element)
|
|
||||||
}
|
|
||||||
element.redo()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) redo () {
|
func (element *TextBox) clipboardPut (text []rune) {
|
||||||
if element.core.HasImage () {
|
window := element.entity.Window()
|
||||||
element.draw()
|
if window != nil {
|
||||||
element.core.DamageAll()
|
window.Copy(data.Bytes(data.MimePlain, []byte(string(text))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *TextBox) draw () {
|
func (element *TextBox) state () tomo.State {
|
||||||
bounds := element.Bounds()
|
return tomo.State {
|
||||||
|
|
||||||
state := tomo.State {
|
|
||||||
Disabled: !element.Enabled(),
|
Disabled: !element.Enabled(),
|
||||||
Focused: element.Focused(),
|
Focused: element.entity.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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
129
entity.go
Normal file
129
entity.go
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
package tomo
|
||||||
|
|
||||||
|
import "image"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
||||||
|
|
||||||
|
// Entity is a handle given to elements by the backend. Different types of
|
||||||
|
// entities may be assigned to elements that support different capabilities.
|
||||||
|
type Entity interface {
|
||||||
|
// Invalidate marks the element's current visual as invalid. At the end
|
||||||
|
// of every event, the backend will ask all invalid entities to redraw
|
||||||
|
// themselves.
|
||||||
|
Invalidate ()
|
||||||
|
|
||||||
|
// Bounds returns the bounds of the element to be used for drawing and
|
||||||
|
// layout.
|
||||||
|
Bounds () image.Rectangle
|
||||||
|
|
||||||
|
// Window returns the window that the element is in.
|
||||||
|
Window () Window
|
||||||
|
|
||||||
|
// SetMinimumSize reports to the system what the element's minimum size
|
||||||
|
// can be. The minimum size of child elements should be taken into
|
||||||
|
// account when calculating this.
|
||||||
|
SetMinimumSize (width, height int)
|
||||||
|
|
||||||
|
// DrawBackground asks the parent element to draw its background pattern
|
||||||
|
// to a canvas. This should be used for transparent elements like text
|
||||||
|
// labels. If there is no parent element (that is, the element is
|
||||||
|
// directly inside of the window), the backend will draw a default
|
||||||
|
// background pattern.
|
||||||
|
DrawBackground (canvas.Canvas)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LayoutEntity is given to elements that support the Layoutable interface.
|
||||||
|
type LayoutEntity interface {
|
||||||
|
Entity
|
||||||
|
|
||||||
|
// InvalidateLayout marks the element's layout as invalid. At the end of
|
||||||
|
// every event, the backend will ask all invalid elements to recalculate
|
||||||
|
// their layouts.
|
||||||
|
InvalidateLayout ()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContainerEntity is given to elements that support the Container interface.
|
||||||
|
type ContainerEntity interface {
|
||||||
|
Entity
|
||||||
|
LayoutEntity
|
||||||
|
|
||||||
|
// Adopt adds an element as a child.
|
||||||
|
Adopt (child Element)
|
||||||
|
|
||||||
|
// Insert inserts an element in the child list at the specified
|
||||||
|
// location.
|
||||||
|
Insert (index int, child Element)
|
||||||
|
|
||||||
|
// Disown removes the child at the specified index.
|
||||||
|
Disown (index int)
|
||||||
|
|
||||||
|
// IndexOf returns the index of the specified child.
|
||||||
|
IndexOf (child Element) int
|
||||||
|
|
||||||
|
// Child returns the child at the specified index.
|
||||||
|
Child (index int) Element
|
||||||
|
|
||||||
|
// CountChildren returns the amount of children the element has.
|
||||||
|
CountChildren () int
|
||||||
|
|
||||||
|
// PlaceChild sets the size and position of the child at the specified
|
||||||
|
// index to a bounding rectangle.
|
||||||
|
PlaceChild (index int, bounds image.Rectangle)
|
||||||
|
|
||||||
|
// SelectChild marks a child as selected or unselected, if it is
|
||||||
|
// selectable.
|
||||||
|
SelectChild (index int, selected bool)
|
||||||
|
|
||||||
|
// ChildMinimumSize returns the minimum size of the child at the
|
||||||
|
// specified index.
|
||||||
|
ChildMinimumSize (index int) (width, height int)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FocusableEntity is given to elements that support the Focusable interface.
|
||||||
|
type FocusableEntity interface {
|
||||||
|
Entity
|
||||||
|
|
||||||
|
// Focused returns whether the element currently has input focus.
|
||||||
|
Focused () bool
|
||||||
|
|
||||||
|
// Focus sets this element as focused. If this succeeds, the element will
|
||||||
|
// recieve a HandleFocus call.
|
||||||
|
Focus ()
|
||||||
|
|
||||||
|
// FocusNext causes the focus to move to the next element. If this
|
||||||
|
// succeeds, the element will recieve a HandleUnfocus call.
|
||||||
|
FocusNext ()
|
||||||
|
|
||||||
|
// FocusPrevious causes the focus to move to the next element. If this
|
||||||
|
// succeeds, the element will recieve a HandleUnfocus call.
|
||||||
|
FocusPrevious ()
|
||||||
|
}
|
||||||
|
|
||||||
|
// SelectableEntity is given to elements that support the Selectable interface.
|
||||||
|
type SelectableEntity interface {
|
||||||
|
Entity
|
||||||
|
|
||||||
|
// Selected returns whether this element is currently selected.
|
||||||
|
Selected () bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlexibleEntity is given to elements that support the Flexible interface.
|
||||||
|
type FlexibleEntity interface {
|
||||||
|
Entity
|
||||||
|
|
||||||
|
// NotifyFlexibleHeightChange notifies the system that the parameters
|
||||||
|
// affecting the element's flexible height have changed. This method is
|
||||||
|
// expected to be called by flexible elements when their content changes.
|
||||||
|
NotifyFlexibleHeightChange ()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScrollableEntity is given to elements that support the Scrollable interface.
|
||||||
|
type ScrollableEntity interface {
|
||||||
|
Entity
|
||||||
|
|
||||||
|
// NotifyScrollBoundsChange notifies the system that the element's
|
||||||
|
// scroll content bounds or viewport bounds have changed. This is
|
||||||
|
// expected to be called by scrollable elements when they change their
|
||||||
|
// supported scroll axes, their scroll position (either autonomously or
|
||||||
|
// as a result of a call to ScrollTo()), or their content size.
|
||||||
|
NotifyScrollBoundsChange ()
|
||||||
|
}
|
||||||
@@ -4,35 +4,27 @@ import "git.tebibyte.media/sashakoshka/tomo"
|
|||||||
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
import "git.tebibyte.media/sashakoshka/tomo/textdraw"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
}
|
}
|
||||||
|
|
||||||
func 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")
|
window.SetTitle("Text alignment")
|
||||||
|
|
||||||
container := containers.NewDocumentContainer()
|
left := elements.NewLabelWrapped(text)
|
||||||
scrollContainer := containers.NewScrollContainer(false, true)
|
center := elements.NewLabelWrapped(text)
|
||||||
scrollContainer.Adopt(container)
|
right := elements.NewLabelWrapped(text)
|
||||||
window.Adopt(scrollContainer)
|
justify := elements.NewLabelWrapped(text)
|
||||||
|
|
||||||
left := elements.NewLabel(text, true)
|
|
||||||
center := elements.NewLabel(text, true)
|
|
||||||
right := elements.NewLabel(text, true)
|
|
||||||
justify := elements.NewLabel(text, true)
|
|
||||||
|
|
||||||
left.SetAlign(textdraw.AlignLeft)
|
left.SetAlign(textdraw.AlignLeft)
|
||||||
center.SetAlign(textdraw.AlignCenter)
|
center.SetAlign(textdraw.AlignCenter)
|
||||||
right.SetAlign(textdraw.AlignRight)
|
right.SetAlign(textdraw.AlignRight)
|
||||||
justify.SetAlign(textdraw.AlignJustify)
|
justify.SetAlign(textdraw.AlignJustify)
|
||||||
|
|
||||||
container.Adopt(left, true)
|
window.Adopt (elements.NewScroll (elements.ScrollVertical,
|
||||||
container.Adopt(center, true)
|
elements.NewDocument(left, center, right, justify)))
|
||||||
container.Adopt(right, true)
|
|
||||||
container.Adopt(justify, true)
|
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ package main
|
|||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
@@ -15,23 +13,15 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||||
window.SetTitle("Checkboxes")
|
window.SetTitle("Checkboxes")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
introText := elements.NewLabelWrapped (
|
||||||
window.Adopt(container)
|
|
||||||
|
|
||||||
introText := elements.NewLabel (
|
|
||||||
"We advise you to not read thPlease listen to me. I am " +
|
"We advise you to not read thPlease listen to me. I am " +
|
||||||
"trapped inside the example code. This is the only way for " +
|
"trapped inside the example code. This is the only way for " +
|
||||||
"me to communicate.", true)
|
"me to communicate.")
|
||||||
introText.EmCollapse(0, 5)
|
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 := elements.NewCheckbox("We are but their helpless prey", false)
|
||||||
disabledCheckbox.SetEnabled(false)
|
disabledCheckbox.SetEnabled(false)
|
||||||
container.Adopt(disabledCheckbox, false)
|
|
||||||
vsync := elements.NewCheckbox("Enable vsync", false)
|
vsync := elements.NewCheckbox("Enable vsync", false)
|
||||||
vsync.OnToggle (func () {
|
vsync.OnToggle (func () {
|
||||||
if vsync.Value() {
|
if vsync.Value() {
|
||||||
@@ -42,12 +32,23 @@ func run () {
|
|||||||
"That doesn't do anything.")
|
"That doesn't do anything.")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
container.Adopt(vsync, false)
|
|
||||||
button := elements.NewButton("What")
|
button := elements.NewButton("What")
|
||||||
button.OnClick(tomo.Stop)
|
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.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,8 @@ import _ "image/jpeg"
|
|||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/data"
|
import "git.tebibyte.media/sashakoshka/tomo/data"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -27,9 +25,9 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0))
|
||||||
window.SetTitle("Clipboard")
|
window.SetTitle("Clipboard")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
textInput := elements.NewTextBox("", "")
|
textInput := elements.NewTextBox("", "")
|
||||||
controlRow := containers.NewContainer(layouts.Horizontal { true, false })
|
controlRow := elements.NewHBox(elements.SpaceMargin)
|
||||||
copyButton := elements.NewButton("Copy")
|
copyButton := elements.NewButton("Copy")
|
||||||
copyButton.SetIcon(tomo.IconCopy)
|
copyButton.SetIcon(tomo.IconCopy)
|
||||||
pasteButton := elements.NewButton("Paste")
|
pasteButton := elements.NewButton("Paste")
|
||||||
@@ -109,11 +107,11 @@ func run () {
|
|||||||
window.Paste(imageClipboardCallback, validImageTypes...)
|
window.Paste(imageClipboardCallback, validImageTypes...)
|
||||||
})
|
})
|
||||||
|
|
||||||
container.Adopt(textInput, true)
|
container.AdoptExpand(textInput)
|
||||||
controlRow.Adopt(copyButton, true)
|
controlRow.AdoptExpand(copyButton)
|
||||||
controlRow.Adopt(pasteButton, true)
|
controlRow.AdoptExpand(pasteButton)
|
||||||
controlRow.Adopt(pasteImageButton, true)
|
controlRow.AdoptExpand(pasteImageButton)
|
||||||
container.Adopt(controlRow, false)
|
container.Adopt(controlRow)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
@@ -123,13 +121,15 @@ func run () {
|
|||||||
func imageWindow (parent tomo.Window, image image.Image) {
|
func imageWindow (parent tomo.Window, image image.Image) {
|
||||||
window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0))
|
window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0))
|
||||||
window.SetTitle("Clipboard Image")
|
window.SetTitle("Clipboard Image")
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
closeButton := elements.NewButton("Ok")
|
closeButton := elements.NewButton("Ok")
|
||||||
closeButton.SetIcon(tomo.IconYes)
|
closeButton.SetIcon(tomo.IconYes)
|
||||||
closeButton.OnClick(window.Close)
|
closeButton.OnClick(window.Close)
|
||||||
|
|
||||||
container.Adopt(elements.NewImage(image), true)
|
container.AdoptExpand(elements.NewImage(image))
|
||||||
container.Adopt(closeButton, false)
|
container.Adopt(closeButton)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
|
closeButton.Focus()
|
||||||
window.Show()
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -22,47 +21,41 @@ func run () {
|
|||||||
file.Close()
|
file.Close()
|
||||||
if err != nil { panic(err.Error()); return }
|
if err != nil { panic(err.Error()); return }
|
||||||
|
|
||||||
scrollContainer := containers.NewScrollContainer(false, true)
|
document := elements.NewDocument()
|
||||||
document := containers.NewDocumentContainer()
|
document.Adopt (
|
||||||
|
elements.NewLabelWrapped (
|
||||||
document.Adopt (elements.NewLabel (
|
"A document container is a vertically stacked container " +
|
||||||
"A document container is a vertically stacked container " +
|
"capable of properly laying out flexible elements such as " +
|
||||||
"capable of properly laying out flexible elements such as " +
|
"text-wrapped labels. You can also include normal elements " +
|
||||||
"text-wrapped labels. You can also include normal elements " +
|
"like:"),
|
||||||
"like:", true), true)
|
elements.NewButton("Buttons,"),
|
||||||
document.Adopt (elements.NewButton (
|
elements.NewCheckbox("Checkboxes,", true),
|
||||||
"Buttons,"), true)
|
elements.NewTextBox("", "And text boxes."),
|
||||||
document.Adopt (elements.NewCheckbox (
|
elements.NewLine(),
|
||||||
"Checkboxes,", true), true)
|
elements.NewLabelWrapped (
|
||||||
document.Adopt(elements.NewTextBox("", "And text boxes."), true)
|
"Document containers are meant to be placed inside of a " +
|
||||||
document.Adopt (elements.NewSpacer(true), true)
|
"ScrollContainer, like this one."),
|
||||||
document.Adopt (elements.NewLabel (
|
elements.NewLabelWrapped (
|
||||||
"Document containers are meant to be placed inside of a " +
|
"You could use document containers to do things like display various " +
|
||||||
"ScrollContainer, like this one.", true), true)
|
"forms of hypertext (like HTML, gemtext, markdown, etc.), " +
|
||||||
document.Adopt (elements.NewLabel (
|
"lay out a settings menu with descriptive label text between " +
|
||||||
"You could use document containers to do things like display various " +
|
"control groups like in iOS, or list comment or chat histories."),
|
||||||
"forms of hypertext (like HTML, gemtext, markdown, etc.), " +
|
elements.NewImage(logo),
|
||||||
"lay out a settings menu with descriptive label text between " +
|
elements.NewLabelWrapped (
|
||||||
"control groups like in iOS, or list comment or chat histories.",
|
"You can also choose whether each element is on its own line " +
|
||||||
true), true)
|
"(sort of like an HTML/CSS block element) or on a line with " +
|
||||||
document.Adopt(elements.NewImage(logo), true)
|
"other adjacent elements (like an HTML/CSS inline element)."))
|
||||||
document.Adopt (elements.NewLabel (
|
document.AdoptInline (
|
||||||
"You can also choose whether each element is on its own line " +
|
elements.NewButton("Just"),
|
||||||
"(sort of like an HTML/CSS block element) or on a line with " +
|
elements.NewButton("like"),
|
||||||
"other adjacent elements (like an HTML/CSS inline element).",
|
elements.NewButton("this."))
|
||||||
true), true)
|
document.Adopt (elements.NewLabelWrapped (
|
||||||
document.Adopt(elements.NewButton("Just"), false)
|
"Oh, you're a switch? Then name all of these switches:"))
|
||||||
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)
|
|
||||||
for i := 0; i < 30; i ++ {
|
for i := 0; i < 30; i ++ {
|
||||||
document.Adopt(elements.NewSwitch("", false), false)
|
document.AdoptInline(elements.NewSwitch("", false))
|
||||||
}
|
}
|
||||||
|
|
||||||
scrollContainer.Adopt(document)
|
window.Adopt(elements.NewScroll(elements.ScrollVertical, document))
|
||||||
window.Adopt(scrollContainer)
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,7 @@ package main
|
|||||||
import "os"
|
import "os"
|
||||||
import "path/filepath"
|
import "path/filepath"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/file"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
@@ -16,11 +13,11 @@ func main () {
|
|||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 384, 384))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 384, 384))
|
||||||
window.SetTitle("File browser")
|
window.SetTitle("File browser")
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
homeDir, _ := os.UserHomeDir()
|
homeDir, _ := os.UserHomeDir()
|
||||||
|
|
||||||
controlBar := containers.NewContainer(layouts.Horizontal { })
|
controlBar := elements.NewHBox(elements.SpaceNone)
|
||||||
backButton := elements.NewButton("Back")
|
backButton := elements.NewButton("Back")
|
||||||
backButton.SetIcon(tomo.IconBackward)
|
backButton.SetIcon(tomo.IconBackward)
|
||||||
backButton.ShowText(false)
|
backButton.ShowText(false)
|
||||||
@@ -35,12 +32,11 @@ func run () {
|
|||||||
upwardButton.ShowText(false)
|
upwardButton.ShowText(false)
|
||||||
locationInput := elements.NewTextBox("Location", "")
|
locationInput := elements.NewTextBox("Location", "")
|
||||||
|
|
||||||
statusBar := containers.NewContainer(layouts.Horizontal { true, false })
|
statusBar := elements.NewHBox(elements.SpaceMargin)
|
||||||
directory, _ := fileElements.NewFile(homeDir, nil)
|
directory, _ := elements.NewFile(homeDir, nil)
|
||||||
baseName := elements.NewLabel(filepath.Base(homeDir), false)
|
baseName := elements.NewLabel(filepath.Base(homeDir))
|
||||||
|
|
||||||
scrollContainer := containers.NewScrollContainer(false, true)
|
directoryView, _ := elements.NewDirectory(homeDir, nil)
|
||||||
directoryView, _ := fileElements.NewDirectory(homeDir, nil)
|
|
||||||
updateStatus := func () {
|
updateStatus := func () {
|
||||||
filePath, _ := directoryView.Location()
|
filePath, _ := directoryView.Location()
|
||||||
directory.SetLocation(filePath, nil)
|
directory.SetLocation(filePath, nil)
|
||||||
@@ -73,18 +69,14 @@ func run () {
|
|||||||
choose(filepath.Dir(filePath))
|
choose(filepath.Dir(filePath))
|
||||||
})
|
})
|
||||||
|
|
||||||
controlBar.Adopt(backButton, false)
|
controlBar.Adopt(backButton, forwardButton, refreshButton, upwardButton)
|
||||||
controlBar.Adopt(forwardButton, false)
|
controlBar.AdoptExpand(locationInput)
|
||||||
controlBar.Adopt(refreshButton, false)
|
statusBar.Adopt(directory, baseName)
|
||||||
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(controlBar)
|
||||||
container.Adopt(scrollContainer, true)
|
container.AdoptExpand (
|
||||||
container.Adopt(statusBar, false)
|
elements.NewScroll(elements.ScrollVertical, directoryView))
|
||||||
|
container.Adopt(statusBar)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package main
|
|||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/flow"
|
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/elements"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -14,15 +12,15 @@ func main () {
|
|||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192))
|
||||||
window.SetTitle("adventure")
|
window.SetTitle("adventure")
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
var world flow.Flow
|
var world flow.Flow
|
||||||
world.Transition = container.DisownAll
|
world.Transition = container.DisownAll
|
||||||
world.Stages = map [string] func () {
|
world.Stages = map [string] func () {
|
||||||
"start": func () {
|
"start": func () {
|
||||||
label := elements.NewLabel (
|
label := elements.NewLabelWrapped (
|
||||||
"you are standing next to a river.", true)
|
"you are standing next to a river.")
|
||||||
|
|
||||||
button0 := elements.NewButton("go in the river")
|
button0 := elements.NewButton("go in the river")
|
||||||
button0.OnClick(world.SwitchFunc("wet"))
|
button0.OnClick(world.SwitchFunc("wet"))
|
||||||
@@ -31,81 +29,66 @@ func run () {
|
|||||||
button2 := elements.NewButton("turn around")
|
button2 := elements.NewButton("turn around")
|
||||||
button2.OnClick(world.SwitchFunc("bear"))
|
button2.OnClick(world.SwitchFunc("bear"))
|
||||||
|
|
||||||
container.Warp ( func () {
|
container.AdoptExpand(label)
|
||||||
container.Adopt(label, true)
|
container.Adopt(button0, button1, button2)
|
||||||
container.Adopt(button0, false)
|
button0.Focus()
|
||||||
container.Adopt(button1, false)
|
|
||||||
container.Adopt(button2, false)
|
|
||||||
button0.Focus()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
"wet": func () {
|
"wet": func () {
|
||||||
label := elements.NewLabel (
|
label := elements.NewLabelWrapped (
|
||||||
"you get completely soaked.\n" +
|
"you get completely soaked.\n" +
|
||||||
"you die of hypothermia.", true)
|
"you die of hypothermia.")
|
||||||
|
|
||||||
button0 := elements.NewButton("try again")
|
button0 := elements.NewButton("try again")
|
||||||
button0.OnClick(world.SwitchFunc("start"))
|
button0.OnClick(world.SwitchFunc("start"))
|
||||||
button1 := elements.NewButton("exit")
|
button1 := elements.NewButton("exit")
|
||||||
button1.OnClick(tomo.Stop)
|
button1.OnClick(tomo.Stop)
|
||||||
|
|
||||||
container.Warp (func () {
|
container.AdoptExpand(label)
|
||||||
container.Adopt(label, true)
|
container.Adopt(button0, button1)
|
||||||
container.Adopt(button0, false)
|
button0.Focus()
|
||||||
container.Adopt(button1, false)
|
|
||||||
button0.Focus()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
"house": func () {
|
"house": func () {
|
||||||
label := elements.NewLabel (
|
label := elements.NewLabelWrapped (
|
||||||
"you are standing in front of a delapidated " +
|
"you are standing in front of a delapidated " +
|
||||||
"house.", true)
|
"house.")
|
||||||
|
|
||||||
button1 := elements.NewButton("go inside")
|
button1 := elements.NewButton("go inside")
|
||||||
button1.OnClick(world.SwitchFunc("inside"))
|
button1.OnClick(world.SwitchFunc("inside"))
|
||||||
button0 := elements.NewButton("turn back")
|
button0 := elements.NewButton("turn back")
|
||||||
button0.OnClick(world.SwitchFunc("start"))
|
button0.OnClick(world.SwitchFunc("start"))
|
||||||
|
|
||||||
container.Warp (func () {
|
container.AdoptExpand(label)
|
||||||
container.Adopt(label, true)
|
container.Adopt(button0, button1)
|
||||||
container.Adopt(button1, false)
|
button1.Focus()
|
||||||
container.Adopt(button0, false)
|
|
||||||
button1.Focus()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
"inside": func () {
|
"inside": func () {
|
||||||
label := elements.NewLabel (
|
label := elements.NewLabelWrapped (
|
||||||
"you are standing inside of the house.\n" +
|
"you are standing inside of the house.\n" +
|
||||||
"it is dark, but rays of light stream " +
|
"it is dark, but rays of light stream " +
|
||||||
"through the window.\n" +
|
"through the window.\n" +
|
||||||
"there is nothing particularly interesting " +
|
"there is nothing particularly interesting " +
|
||||||
"here.", true)
|
"here.")
|
||||||
|
|
||||||
button0 := elements.NewButton("go back outside")
|
button0 := elements.NewButton("go back outside")
|
||||||
button0.OnClick(world.SwitchFunc("house"))
|
button0.OnClick(world.SwitchFunc("house"))
|
||||||
|
|
||||||
container.Warp (func () {
|
container.AdoptExpand(label)
|
||||||
container.Adopt(label, true)
|
container.Adopt(button0)
|
||||||
container.Adopt(button0, false)
|
button0.Focus()
|
||||||
button0.Focus()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
"bear": func () {
|
"bear": func () {
|
||||||
label := elements.NewLabel (
|
label := elements.NewLabelWrapped (
|
||||||
"you come face to face with a bear.\n" +
|
"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 := elements.NewButton("try again")
|
||||||
button0.OnClick(world.SwitchFunc("start"))
|
button0.OnClick(world.SwitchFunc("start"))
|
||||||
button1 := elements.NewButton("exit")
|
button1 := elements.NewButton("exit")
|
||||||
button1.OnClick(tomo.Stop)
|
button1.OnClick(tomo.Stop)
|
||||||
|
|
||||||
container.Warp (func () {
|
container.AdoptExpand(label)
|
||||||
container.Adopt(label, true)
|
container.Adopt(button0, button1)
|
||||||
container.Adopt(button0, false)
|
button0.Focus()
|
||||||
container.Adopt(button1, false)
|
|
||||||
button0.Focus()
|
|
||||||
})
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
world.Switch("start")
|
world.Switch("start")
|
||||||
|
|||||||
@@ -3,11 +3,9 @@ package main
|
|||||||
import "os"
|
import "os"
|
||||||
import "time"
|
import "time"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -17,13 +15,14 @@ func main () {
|
|||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216))
|
||||||
window.SetTitle("Clock")
|
window.SetTitle("Clock")
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
window.SetApplicationName("TomoClock")
|
||||||
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
clock := fun.NewAnalogClock(time.Now())
|
clock := fun.NewAnalogClock(time.Now())
|
||||||
container.Adopt(clock, true)
|
label := elements.NewLabel(formatTime())
|
||||||
label := elements.NewLabel(formatTime(), false)
|
container.AdoptExpand(clock)
|
||||||
container.Adopt(label, false)
|
container.Adopt(label)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
|
|||||||
24
examples/hbox/main.go
Normal file
24
examples/hbox/main.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo/elements"
|
||||||
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
|
|
||||||
|
func main () {
|
||||||
|
tomo.Run(run)
|
||||||
|
}
|
||||||
|
|
||||||
|
func run () {
|
||||||
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
|
||||||
|
window.SetTitle("horizontal stack")
|
||||||
|
|
||||||
|
container := elements.NewHBox(elements.SpaceBoth)
|
||||||
|
window.Adopt(container)
|
||||||
|
|
||||||
|
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
|
||||||
|
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
|
||||||
|
container.AdoptExpand(elements.NewLabelWrapped("this is sample text"))
|
||||||
|
|
||||||
|
window.OnClose(tomo.Stop)
|
||||||
|
window.Show()
|
||||||
|
}
|
||||||
@@ -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
|
package main
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -14,30 +12,31 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0))
|
||||||
window.SetTitle("Icons")
|
window.SetTitle("Icons")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
container.Adopt(elements.NewLabel("Just some of the wonderful icons we have:", false), false)
|
container.Adopt (
|
||||||
container.Adopt(elements.NewSpacer(true), false)
|
elements.NewLabel("Just some of the wonderful icons we have:"),
|
||||||
container.Adopt(icons(tomo.IconHome, tomo.IconHistory), true)
|
elements.NewLine())
|
||||||
container.Adopt(icons(tomo.IconFile, tomo.IconNetwork), true)
|
container.AdoptExpand (
|
||||||
container.Adopt(icons(tomo.IconOpen, tomo.IconRemoveFavorite), true)
|
icons(tomo.IconHome, tomo.IconHistory),
|
||||||
container.Adopt(icons(tomo.IconCursor, tomo.IconDistort), true)
|
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.SetIcon(tomo.IconYes)
|
||||||
closeButton.ShowText(false)
|
|
||||||
closeButton.OnClick(tomo.Stop)
|
closeButton.OnClick(tomo.Stop)
|
||||||
container.Adopt(closeButton, false)
|
container.Adopt(closeButton)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
}
|
}
|
||||||
|
|
||||||
func icons (min, max tomo.Icon) (container *containers.Container) {
|
func icons (min, max tomo.Icon) (container *elements.Box) {
|
||||||
container = containers.NewContainer(layouts.Horizontal { true, false })
|
container = elements.NewHBox(elements.SpaceMargin)
|
||||||
for index := min; index <= max; index ++ {
|
for index := min; index <= max; index ++ {
|
||||||
container.Adopt(elements.NewIcon(index, tomo.IconSizeSmall), true)
|
container.AdoptExpand(elements.NewIcon(index, tomo.IconSizeSmall))
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,15 @@ import _ "image/png"
|
|||||||
import "github.com/jezek/xgbutil/gopher"
|
import "github.com/jezek/xgbutil/gopher"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
}
|
}
|
||||||
|
|
||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(2, 2)
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||||
window.SetTitle("Tomo Logo")
|
window.SetTitle("Tomo Logo")
|
||||||
|
|
||||||
file, err := os.Open("assets/banner.png")
|
file, err := os.Open("assets/banner.png")
|
||||||
@@ -26,19 +24,20 @@ func run () {
|
|||||||
file.Close()
|
file.Close()
|
||||||
if err != nil { fatalError(window, err); return }
|
if err != nil { fatalError(window, err); return }
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
logoImage := elements.NewImage(logo)
|
logoImage := elements.NewImage(logo)
|
||||||
button := elements.NewButton("Show me a gopher instead")
|
button := elements.NewButton("Show me a gopher instead")
|
||||||
button.OnClick (func () { container.Warp (func () {
|
button.OnClick (func () {
|
||||||
container.DisownAll()
|
window.SetTitle("Not the Tomo Logo")
|
||||||
gopher, _, err :=
|
container.DisownAll()
|
||||||
image.Decode(bytes.NewReader(gopher.GopherPng()))
|
gopher, _, err :=
|
||||||
if err != nil { fatalError(window, err); return }
|
image.Decode(bytes.NewReader(gopher.GopherPng()))
|
||||||
container.Adopt(elements.NewImage(gopher),true)
|
if err != nil { fatalError(window, err); return }
|
||||||
}) })
|
container.AdoptExpand(elements.NewImage(gopher))
|
||||||
|
})
|
||||||
|
|
||||||
container.Adopt(logoImage, true)
|
container.AdoptExpand(logoImage)
|
||||||
container.Adopt(button, false)
|
container.Adopt(button)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
button.Focus()
|
button.Focus()
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package main
|
|||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -14,7 +12,7 @@ func main () {
|
|||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||||
window.SetTitle("Enter Details")
|
window.SetTitle("Enter Details")
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
// create inputs
|
// create inputs
|
||||||
@@ -47,13 +45,8 @@ func run () {
|
|||||||
fingerLength.OnChange(check)
|
fingerLength.OnChange(check)
|
||||||
|
|
||||||
// add elements to container
|
// add elements to container
|
||||||
container.Adopt(elements.NewLabel("Choose your words carefully.", false), true)
|
container.AdoptExpand(elements.NewLabel("Choose your words carefully."))
|
||||||
container.Adopt(firstName, false)
|
container.Adopt(firstName, lastName, fingerLength, elements.NewLine(), button)
|
||||||
container.Adopt(lastName, false)
|
|
||||||
container.Adopt(fingerLength, false)
|
|
||||||
container.Adopt(elements.NewSpacer(true), false)
|
|
||||||
container.Adopt(button, false)
|
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ func main () {
|
|||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360))
|
||||||
window.SetTitle("example label")
|
window.SetTitle("example label")
|
||||||
window.Adopt(elements.NewLabel(text, true))
|
window.Adopt(elements.NewLabelWrapped(text))
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package main
|
|||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
@@ -16,47 +14,54 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0))
|
||||||
window.SetTitle("List Sidebar")
|
window.SetTitle("List Sidebar")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Horizontal { true, true })
|
container := elements.NewHBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
var currentPage tomo.Element
|
var currentPage tomo.Element
|
||||||
turnPage := func (newPage tomo.Element) {
|
turnPage := func (newPage tomo.Element) {
|
||||||
container.Warp (func () {
|
if currentPage != nil {
|
||||||
if currentPage != nil {
|
container.Disown(currentPage)
|
||||||
container.Disown(currentPage)
|
}
|
||||||
}
|
container.AdoptExpand(newPage)
|
||||||
container.Adopt(newPage, true)
|
currentPage = newPage
|
||||||
currentPage = newPage
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
intro := elements.NewLabel (
|
intro := elements.NewLabelWrapped (
|
||||||
"The List element can be easily used as a sidebar. " +
|
"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 := elements.NewButton("I do nothing!")
|
||||||
button.OnClick (func () {
|
button.OnClick (func () {
|
||||||
popups.NewDialog(popups.DialogKindInfo, window, "", "Sike!")
|
popups.NewDialog(popups.DialogKindInfo, window, "", "Sike!")
|
||||||
})
|
})
|
||||||
mouse := testing.NewMouse()
|
mouse := testing.NewMouse()
|
||||||
input := elements.NewTextBox("Write some text", "")
|
input := elements.NewTextBox("Write some text", "")
|
||||||
form := containers.NewContainer(layouts.Vertical { true, false})
|
form := elements.NewVBox (
|
||||||
form.Adopt(elements.NewLabel("I have:", false), false)
|
elements.SpaceMargin,
|
||||||
form.Adopt(elements.NewSpacer(true), false)
|
elements.NewLabel("I have:"),
|
||||||
form.Adopt(elements.NewCheckbox("Skin", true), false)
|
elements.NewLine(),
|
||||||
form.Adopt(elements.NewCheckbox("Blood", false), false)
|
elements.NewCheckbox("Skin", true),
|
||||||
form.Adopt(elements.NewCheckbox("Bone", false), false)
|
elements.NewCheckbox("Blood", false),
|
||||||
|
elements.NewCheckbox("Bone", false))
|
||||||
art := testing.NewArtist()
|
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 (
|
list := elements.NewList (
|
||||||
elements.NewListEntry("button", func () { turnPage(button) }),
|
1,
|
||||||
elements.NewListEntry("mouse", func () { turnPage(mouse) }),
|
makePage("button", func () { turnPage(button) }),
|
||||||
elements.NewListEntry("input", func () { turnPage(input) }),
|
makePage("mouse", func () { turnPage(mouse) }),
|
||||||
elements.NewListEntry("form", func () { turnPage(form) }),
|
makePage("input", func () { turnPage(input) }),
|
||||||
elements.NewListEntry("art", func () { turnPage(art) }))
|
makePage("form", func () { turnPage(form) }),
|
||||||
list.OnNoEntrySelected(func () { turnPage (intro) })
|
makePage("art", func () { turnPage(art) }))
|
||||||
list.Collapse(96, 0)
|
list.Collapse(96, 0)
|
||||||
|
|
||||||
container.Adopt(list, false)
|
container.Adopt(list)
|
||||||
turnPage(intro)
|
turnPage(intro)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
|
|||||||
@@ -3,10 +3,8 @@ package main
|
|||||||
import "fmt"
|
import "fmt"
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -16,8 +14,9 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256))
|
window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256))
|
||||||
window.SetTitle("Main")
|
window.SetTitle("Main")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox (
|
||||||
container.Adopt(elements.NewLabel("Main window", false), true)
|
elements.SpaceBoth,
|
||||||
|
elements.NewLabel("Main window"))
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
@@ -33,8 +32,9 @@ func createPanel (parent tomo.MainWindow, id int, bounds image.Rectangle) {
|
|||||||
window, _ := parent.NewPanel(bounds)
|
window, _ := parent.NewPanel(bounds)
|
||||||
title := fmt.Sprint("Panel #", id)
|
title := fmt.Sprint("Panel #", id)
|
||||||
window.SetTitle(title)
|
window.SetTitle(title)
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox (
|
||||||
container.Adopt(elements.NewLabel(title, false), true)
|
elements.SpaceBoth,
|
||||||
|
elements.NewLabel(title))
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
window.Show()
|
window.Show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package main
|
|||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -16,10 +14,10 @@ func run () {
|
|||||||
if err != nil { panic(err.Error()) }
|
if err != nil { panic(err.Error()) }
|
||||||
window.SetTitle("Dialog Boxes")
|
window.SetTitle("Dialog Boxes")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
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 := elements.NewButton("popups.DialogKindInfo")
|
||||||
infoButton.OnClick (func () {
|
infoButton.OnClick (func () {
|
||||||
@@ -29,7 +27,7 @@ func run () {
|
|||||||
"Information",
|
"Information",
|
||||||
"You are wacky")
|
"You are wacky")
|
||||||
})
|
})
|
||||||
container.Adopt(infoButton, false)
|
container.Adopt(infoButton)
|
||||||
infoButton.Focus()
|
infoButton.Focus()
|
||||||
|
|
||||||
questionButton := elements.NewButton("popups.DialogKindQuestion")
|
questionButton := elements.NewButton("popups.DialogKindQuestion")
|
||||||
@@ -43,7 +41,7 @@ func run () {
|
|||||||
popups.Button { "No", func () { } },
|
popups.Button { "No", func () { } },
|
||||||
popups.Button { "Not sure", func () { } })
|
popups.Button { "Not sure", func () { } })
|
||||||
})
|
})
|
||||||
container.Adopt(questionButton, false)
|
container.Adopt(questionButton)
|
||||||
|
|
||||||
warningButton := elements.NewButton("popups.DialogKindWarning")
|
warningButton := elements.NewButton("popups.DialogKindWarning")
|
||||||
warningButton.OnClick (func () {
|
warningButton.OnClick (func () {
|
||||||
@@ -53,7 +51,7 @@ func run () {
|
|||||||
"Warning",
|
"Warning",
|
||||||
"They are fast approaching.")
|
"They are fast approaching.")
|
||||||
})
|
})
|
||||||
container.Adopt(warningButton, false)
|
container.Adopt(warningButton)
|
||||||
|
|
||||||
errorButton := elements.NewButton("popups.DialogKindError")
|
errorButton := elements.NewButton("popups.DialogKindError")
|
||||||
errorButton.OnClick (func () {
|
errorButton.OnClick (func () {
|
||||||
@@ -63,22 +61,23 @@ func run () {
|
|||||||
"Error",
|
"Error",
|
||||||
"There is nowhere left to go.")
|
"There is nowhere left to go.")
|
||||||
})
|
})
|
||||||
container.Adopt(errorButton, false)
|
container.Adopt(errorButton)
|
||||||
|
|
||||||
menuButton := elements.NewButton("menu")
|
menuButton := elements.NewButton("menu")
|
||||||
menuButton.OnClick (func () {
|
menuButton.OnClick (func () {
|
||||||
|
// TODO: make a better way to get the bounds of something
|
||||||
menu, err := window.NewMenu (
|
menu, err := window.NewMenu (
|
||||||
tomo.Bounds(0, 0, 64, 64).
|
tomo.Bounds(0, 0, 64, 64).
|
||||||
Add(menuButton.Bounds().Min))
|
Add(menuButton.Entity().Bounds().Min))
|
||||||
if err != nil { println(err.Error()) }
|
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()
|
menu.Show()
|
||||||
})
|
})
|
||||||
container.Adopt(menuButton, false)
|
container.Adopt(menuButton)
|
||||||
|
|
||||||
cancelButton := elements.NewButton("No thank you.")
|
cancelButton := elements.NewButton("No thank you.")
|
||||||
cancelButton.OnClick(tomo.Stop)
|
cancelButton.OnClick(tomo.Stop)
|
||||||
container.Adopt(cancelButton, false)
|
container.Adopt(cancelButton)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ package main
|
|||||||
import "time"
|
import "time"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
@@ -15,16 +13,15 @@ func main () {
|
|||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||||
window.SetTitle("Approaching")
|
window.SetTitle("Approaching")
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
container.Adopt (elements.NewLabel (
|
container.AdoptExpand(elements.NewLabel("Rapidly approaching your location..."))
|
||||||
"Rapidly approaching your location...", false), false)
|
|
||||||
bar := elements.NewProgressBar(0)
|
bar := elements.NewProgressBar(0)
|
||||||
container.Adopt(bar, false)
|
container.Adopt(bar)
|
||||||
button := elements.NewButton("Stop")
|
button := elements.NewButton("Stop")
|
||||||
button.SetEnabled(false)
|
button.SetEnabled(false)
|
||||||
container.Adopt(button, false)
|
container.Adopt(button)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
import "image"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/canvas"
|
|
||||||
|
|
||||||
type Game struct {
|
type Game struct {
|
||||||
*Raycaster
|
*Raycaster
|
||||||
@@ -31,21 +29,17 @@ func NewGame (world World, textures Textures) (game *Game) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) DrawTo (
|
func (game *Game) Start () {
|
||||||
canvas canvas.Canvas,
|
if game.running == true { return }
|
||||||
bounds image.Rectangle,
|
game.running = true
|
||||||
onDamage func (image.Rectangle),
|
go game.run()
|
||||||
) {
|
}
|
||||||
if canvas == nil {
|
|
||||||
select {
|
func (game *Game) Stop () {
|
||||||
case game.stopChan <- true:
|
select {
|
||||||
default:
|
case game.stopChan <- true:
|
||||||
}
|
default:
|
||||||
} else if !game.running {
|
|
||||||
game.running = true
|
|
||||||
go game.run()
|
|
||||||
}
|
}
|
||||||
game.Raycaster.DrawTo(canvas, bounds, onDamage)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (game *Game) Stamina () float64 {
|
func (game *Game) Stamina () float64 {
|
||||||
@@ -110,7 +104,7 @@ func (game *Game) tick () {
|
|||||||
game.stamina = 0
|
game.stamina = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
tomo.Do(game.Draw)
|
tomo.Do(game.Invalidate)
|
||||||
if statUpdate && game.onStatUpdate != nil {
|
if statUpdate && game.onStatUpdate != nil {
|
||||||
tomo.Do(game.onStatUpdate)
|
tomo.Do(game.onStatUpdate)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,10 +5,8 @@ import _ "embed"
|
|||||||
import _ "image/png"
|
import _ "image/png"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/popups"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
//go:embed wall.png
|
//go:embed wall.png
|
||||||
var wallTextureBytes []uint8
|
var wallTextureBytes []uint8
|
||||||
@@ -17,11 +15,13 @@ func main () {
|
|||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FIXME this entire example seems to be broken
|
||||||
|
|
||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480))
|
||||||
window.SetTitle("Raycaster")
|
window.SetTitle("Raycaster")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { false, false })
|
container := elements.NewVBox(elements.SpaceNone)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes))
|
wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes))
|
||||||
@@ -48,21 +48,22 @@ func run () {
|
|||||||
wallTexture,
|
wallTexture,
|
||||||
})
|
})
|
||||||
|
|
||||||
topBar := containers.NewContainer(layouts.Horizontal { true, true })
|
topBar := elements.NewHBox(elements.SpaceBoth)
|
||||||
staminaBar := elements.NewProgressBar(game.Stamina())
|
staminaBar := elements.NewProgressBar(game.Stamina())
|
||||||
healthBar := elements.NewProgressBar(game.Health())
|
healthBar := elements.NewProgressBar(game.Health())
|
||||||
|
|
||||||
topBar.Adopt(elements.NewLabel("Stamina:", false), false)
|
topBar.Adopt(elements.NewLabel("Stamina:"))
|
||||||
topBar.Adopt(staminaBar, true)
|
topBar.AdoptExpand(staminaBar)
|
||||||
topBar.Adopt(elements.NewLabel("Health:", false), false)
|
topBar.Adopt(elements.NewLabel("Health:"))
|
||||||
topBar.Adopt(healthBar, true)
|
topBar.AdoptExpand(healthBar)
|
||||||
container.Adopt(topBar, false)
|
container.Adopt(topBar)
|
||||||
container.Adopt(game, true)
|
container.AdoptExpand(game.Raycaster)
|
||||||
game.Focus()
|
game.Focus()
|
||||||
|
|
||||||
game.OnStatUpdate (func () {
|
game.OnStatUpdate (func () {
|
||||||
staminaBar.SetProgress(game.Stamina())
|
staminaBar.SetProgress(game.Stamina())
|
||||||
})
|
})
|
||||||
|
game.Start()
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
|
|||||||
@@ -4,10 +4,11 @@ package main
|
|||||||
import "math"
|
import "math"
|
||||||
import "image"
|
import "image"
|
||||||
import "image/color"
|
import "image/color"
|
||||||
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/input"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
import "git.tebibyte.media/sashakoshka/tomo/artist/shapes"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
import "git.tebibyte.media/sashakoshka/tomo/default/config"
|
||||||
|
|
||||||
type ControlState struct {
|
type ControlState struct {
|
||||||
@@ -21,10 +22,8 @@ type ControlState struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Raycaster struct {
|
type Raycaster struct {
|
||||||
*core.Core
|
entity tomo.FocusableEntity
|
||||||
*core.FocusableCore
|
|
||||||
core core.CoreControl
|
|
||||||
focusableControl core.FocusableCoreControl
|
|
||||||
config config.Wrapped
|
config config.Wrapped
|
||||||
|
|
||||||
Camera
|
Camera
|
||||||
@@ -49,31 +48,107 @@ func NewRaycaster (world World, textures Textures) (element *Raycaster) {
|
|||||||
textures: textures,
|
textures: textures,
|
||||||
renderDistance: 8,
|
renderDistance: 8,
|
||||||
}
|
}
|
||||||
element.Core, element.core = core.NewCore(element, element.drawAll)
|
element.entity = tomo.NewEntity(element).(tomo.FocusableEntity)
|
||||||
element.FocusableCore,
|
element.entity.SetMinimumSize(64, 64)
|
||||||
element.focusableControl = core.NewFocusableCore(element.core, element.Draw)
|
|
||||||
element.core.SetMinimumSize(64, 64)
|
|
||||||
return
|
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)) {
|
func (element *Raycaster) OnControlStateChange (callback func (ControlState)) {
|
||||||
element.onControlStateChange = callback
|
element.onControlStateChange = callback
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Raycaster) Draw () {
|
func (element *Raycaster) Focus () {
|
||||||
if element.core.HasImage() {
|
element.entity.Focus()
|
||||||
element.drawAll()
|
|
||||||
element.core.DamageAll()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
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) 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) {
|
func (element *Raycaster) HandleKeyDown (key input.Key, modifiers input.Modifiers) {
|
||||||
switch key {
|
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 {
|
func shadeColor (c color.RGBA, brightness float64) color.RGBA {
|
||||||
return color.RGBA {
|
return color.RGBA {
|
||||||
uint8(float64(c.R) * brightness),
|
uint8(float64(c.R) * brightness),
|
||||||
@@ -187,8 +193,8 @@ func shadeColor (c color.RGBA, brightness float64) color.RGBA {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Raycaster) drawMinimap () {
|
func (element *Raycaster) drawMinimap (destination canvas.Canvas) {
|
||||||
bounds := element.Bounds()
|
bounds := element.entity.Bounds()
|
||||||
scale := 8
|
scale := 8
|
||||||
for y := 0; y < len(element.world.Data) / element.world.Stride; y ++ {
|
for y := 0; y < len(element.world.Data) / element.world.Stride; y ++ {
|
||||||
for x := 0; x < element.world.Stride; x ++ {
|
for x := 0; x < element.world.Stride; x ++ {
|
||||||
@@ -204,7 +210,7 @@ func (element *Raycaster) drawMinimap () {
|
|||||||
cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF }
|
cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF }
|
||||||
}
|
}
|
||||||
shapes.FillColorRectangle (
|
shapes.FillColorRectangle (
|
||||||
element.core,
|
destination,
|
||||||
cellColor,
|
cellColor,
|
||||||
cellBounds.Inset(1))
|
cellBounds.Inset(1))
|
||||||
}}
|
}}
|
||||||
@@ -219,16 +225,16 @@ func (element *Raycaster) drawMinimap () {
|
|||||||
|
|
||||||
playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8)
|
playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8)
|
||||||
shapes.FillColorEllipse (
|
shapes.FillColorEllipse (
|
||||||
element.core,
|
destination,
|
||||||
artist.Hex(0xFFFFFFFF),
|
artist.Hex(0xFFFFFFFF),
|
||||||
playerBounds)
|
playerBounds)
|
||||||
shapes.ColorLine (
|
shapes.ColorLine (
|
||||||
element.core,
|
destination,
|
||||||
artist.Hex(0xFFFFFFFF), 1,
|
artist.Hex(0xFFFFFFFF), 1,
|
||||||
playerPt,
|
playerPt,
|
||||||
playerAnglePt)
|
playerAnglePt)
|
||||||
shapes.ColorLine (
|
shapes.ColorLine (
|
||||||
element.core,
|
destination,
|
||||||
artist.Hex(0x00FF00FF), 1,
|
artist.Hex(0x00FF00FF), 1,
|
||||||
playerPt,
|
playerPt,
|
||||||
hitPt)
|
hitPt)
|
||||||
|
|||||||
@@ -2,10 +2,8 @@ package main
|
|||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -14,37 +12,39 @@ func main () {
|
|||||||
func run () {
|
func run () {
|
||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 240))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 240))
|
||||||
window.SetTitle("Scroll")
|
window.SetTitle("Scroll")
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
textBox := elements.NewTextBox("", copypasta)
|
textBox := elements.NewTextBox("", copypasta)
|
||||||
scrollContainer := containers.NewScrollContainer(true, false)
|
|
||||||
|
|
||||||
disconnectedContainer := containers.NewContainer (layouts.Horizontal {
|
disconnectedContainer := elements.NewHBox(elements.SpaceMargin)
|
||||||
Gap: true,
|
|
||||||
})
|
|
||||||
list := elements.NewList (
|
list := elements.NewList (
|
||||||
elements.NewListEntry("This is list item 0", nil),
|
2,
|
||||||
elements.NewListEntry("This is list item 1", nil),
|
elements.NewCell(elements.NewCheckbox("Item 0", true)),
|
||||||
elements.NewListEntry("This is list item 2", nil),
|
elements.NewCell(elements.NewCheckbox("Item 1", false)),
|
||||||
elements.NewListEntry("This is list item 3", nil),
|
elements.NewCell(elements.NewCheckbox("Item 2", false)),
|
||||||
elements.NewListEntry("This is list item 4", nil),
|
elements.NewCell(elements.NewCheckbox("Item 3", true)),
|
||||||
elements.NewListEntry("This is list item 5", nil),
|
elements.NewCell(elements.NewCheckbox("Item 4", false)),
|
||||||
elements.NewListEntry("This is list item 6", nil),
|
elements.NewCell(elements.NewCheckbox("Item 5", false)),
|
||||||
elements.NewListEntry("This is list item 7", nil),
|
elements.NewCell(elements.NewCheckbox("Item 6", false)),
|
||||||
elements.NewListEntry("This is list item 8", nil),
|
elements.NewCell(elements.NewCheckbox("Item 7", true)),
|
||||||
elements.NewListEntry("This is list item 9", nil),
|
elements.NewCell(elements.NewCheckbox("Item 8", true)),
|
||||||
elements.NewListEntry("This is list item 10", nil),
|
elements.NewCell(elements.NewCheckbox("Item 9", false)),
|
||||||
elements.NewListEntry("This is list item 11", nil),
|
elements.NewCell(elements.NewCheckbox("Item 10", false)),
|
||||||
elements.NewListEntry("This is list item 12", nil),
|
elements.NewCell(elements.NewCheckbox("Item 11", true)),
|
||||||
elements.NewListEntry("This is list item 13", nil),
|
elements.NewCell(elements.NewCheckbox("Item 12", false)),
|
||||||
elements.NewListEntry("This is list item 14", nil),
|
elements.NewCell(elements.NewCheckbox("Item 13", true)),
|
||||||
elements.NewListEntry("This is list item 15", nil),
|
elements.NewCell(elements.NewCheckbox("Item 14", false)),
|
||||||
elements.NewListEntry("This is list item 16", nil),
|
elements.NewCell(elements.NewCheckbox("Item 15", false)),
|
||||||
elements.NewListEntry("This is list item 17", nil),
|
elements.NewCell(elements.NewCheckbox("Item 16", true)),
|
||||||
elements.NewListEntry("This is list item 18", nil),
|
elements.NewCell(elements.NewCheckbox("Item 17", true)),
|
||||||
elements.NewListEntry("This is list item 19", nil),
|
elements.NewCell(elements.NewCheckbox("Item 18", false)),
|
||||||
elements.NewListEntry("This is list item 20", nil))
|
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)
|
list.Collapse(0, 32)
|
||||||
scrollBar := elements.NewScrollBar(true)
|
scrollBar := elements.NewScrollBar(true)
|
||||||
list.OnScrollBoundsChange (func () {
|
list.OnScrollBoundsChange (func () {
|
||||||
@@ -56,17 +56,16 @@ func run () {
|
|||||||
list.ScrollTo(viewport)
|
list.ScrollTo(viewport)
|
||||||
})
|
})
|
||||||
|
|
||||||
scrollContainer.Adopt(textBox)
|
container.Adopt(elements.NewLabel("A ScrollContainer:"))
|
||||||
container.Adopt(elements.NewLabel("A ScrollContainer:", false), false)
|
container.Adopt(elements.NewScroll(elements.ScrollHorizontal, textBox))
|
||||||
container.Adopt(scrollContainer, false)
|
disconnectedContainer.Adopt(list)
|
||||||
disconnectedContainer.Adopt(list, false)
|
disconnectedContainer.AdoptExpand(elements.NewLabelWrapped (
|
||||||
disconnectedContainer.Adopt (elements.NewLabel (
|
|
||||||
"Notice how the scroll bar to the right can be used to " +
|
"Notice how the scroll bar to the right can be used to " +
|
||||||
"control the list, despite not even touching it. It is " +
|
"control the list, despite not even touching it. It is " +
|
||||||
"indeed a thing you can do. It is also terrible UI design so " +
|
"indeed a thing you can do. It is also terrible UI design so " +
|
||||||
"don't do it.", true), true)
|
"don't do it."))
|
||||||
disconnectedContainer.Adopt(scrollBar, false)
|
disconnectedContainer.Adopt(scrollBar)
|
||||||
container.Adopt(disconnectedContainer, true)
|
container.AdoptExpand(disconnectedContainer)
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -14,15 +12,15 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||||
window.SetTitle("Spaced Out")
|
window.SetTitle("Spaced Out")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
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.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)
|
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
window.Show()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
tomo.Run(run)
|
tomo.Run(run)
|
||||||
@@ -14,12 +12,12 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0))
|
||||||
window.SetTitle("Switches")
|
window.SetTitle("Switches")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
window.Adopt(container)
|
||||||
|
|
||||||
container.Adopt(elements.NewSwitch("hahahah", false), false)
|
container.Adopt(elements.NewSwitch("hahahah", false))
|
||||||
container.Adopt(elements.NewSwitch("hehehehheheh", false), false)
|
container.Adopt(elements.NewSwitch("hehehehheheh", false))
|
||||||
container.Adopt(elements.NewSwitch("you can flick da swicth", false), false)
|
container.Adopt(elements.NewSwitch("you can flick da swicth", false))
|
||||||
|
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
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
|
package main
|
||||||
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/all"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
@@ -15,26 +13,25 @@ func run () {
|
|||||||
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128))
|
window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128))
|
||||||
window.SetTitle("vertical stack")
|
window.SetTitle("vertical stack")
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Vertical { true, true })
|
container := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
|
||||||
|
|
||||||
label := elements.NewLabel("it is a label hehe", true)
|
label := elements.NewLabelWrapped("it is a label hehe")
|
||||||
button := elements.NewButton("drawing pad")
|
button := elements.NewButton("drawing pad")
|
||||||
okButton := elements.NewButton("OK")
|
okButton := elements.NewButton("OK")
|
||||||
button.OnClick (func () {
|
button.OnClick (func () {
|
||||||
container.DisownAll()
|
container.DisownAll()
|
||||||
container.Adopt(elements.NewLabel("Draw here:", false), false)
|
container.Adopt(elements.NewLabel("Draw here (not really):"))
|
||||||
container.Adopt(testing.NewMouse(), true)
|
container.AdoptExpand(testing.NewMouse())
|
||||||
container.Adopt(okButton, false)
|
container.Adopt(okButton)
|
||||||
okButton.Focus()
|
okButton.Focus()
|
||||||
})
|
})
|
||||||
okButton.OnClick(tomo.Stop)
|
okButton.OnClick(tomo.Stop)
|
||||||
|
|
||||||
container.Adopt(label, true)
|
container.AdoptExpand(label)
|
||||||
container.Adopt(button, false)
|
container.Adopt(button, okButton)
|
||||||
container.Adopt(okButton, false)
|
window.Adopt(container)
|
||||||
okButton.Focus()
|
|
||||||
|
|
||||||
|
okButton.Focus()
|
||||||
window.OnClose(tomo.Stop)
|
window.OnClose(tomo.Stop)
|
||||||
window.Show()
|
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 "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
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"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/containers"
|
|
||||||
|
|
||||||
// DialogKind defines the semantic role of a dialog window.
|
// DialogKind defines the semantic role of a dialog window.
|
||||||
type DialogKind int
|
type DialogKind int
|
||||||
@@ -16,6 +14,8 @@ const (
|
|||||||
DialogKindError
|
DialogKindError
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO: add ability to have an icon for buttons
|
||||||
|
|
||||||
// Button represents a dialog response button.
|
// Button represents a dialog response button.
|
||||||
type Button struct {
|
type Button struct {
|
||||||
// Name contains the text to display on the button.
|
// Name contains the text to display on the button.
|
||||||
@@ -43,10 +43,10 @@ func NewDialog (
|
|||||||
}
|
}
|
||||||
window.SetTitle(title)
|
window.SetTitle(title)
|
||||||
|
|
||||||
container := containers.NewContainer(layouts.Dialog { true, true })
|
box := elements.NewVBox(elements.SpaceBoth)
|
||||||
window.Adopt(container)
|
messageRow := elements.NewHBox(elements.SpaceMargin)
|
||||||
|
controlRow := elements.NewHBox(elements.SpaceMargin)
|
||||||
|
|
||||||
messageContainer := containers.NewContainer(layouts.Horizontal { true, false })
|
|
||||||
iconId := tomo.IconInformation
|
iconId := tomo.IconInformation
|
||||||
switch kind {
|
switch kind {
|
||||||
case DialogKindInfo: iconId = tomo.IconInformation
|
case DialogKindInfo: iconId = tomo.IconInformation
|
||||||
@@ -55,15 +55,19 @@ func NewDialog (
|
|||||||
case DialogKindError: iconId = tomo.IconError
|
case DialogKindError: iconId = tomo.IconError
|
||||||
}
|
}
|
||||||
|
|
||||||
messageContainer.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge), false)
|
messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge))
|
||||||
messageContainer.Adopt(elements.NewLabel(message, false), true)
|
messageRow.AdoptExpand(elements.NewLabel(message))
|
||||||
container.Adopt(messageContainer, true)
|
|
||||||
|
controlRow.AdoptExpand(elements.NewSpacer())
|
||||||
|
box.AdoptExpand(messageRow)
|
||||||
|
box.Adopt(controlRow)
|
||||||
|
window.Adopt(box)
|
||||||
|
|
||||||
if len(buttons) == 0 {
|
if len(buttons) == 0 {
|
||||||
button := elements.NewButton("OK")
|
button := elements.NewButton("OK")
|
||||||
button.SetIcon(tomo.IconYes)
|
button.SetIcon(tomo.IconYes)
|
||||||
button.OnClick(window.Close)
|
button.OnClick(window.Close)
|
||||||
container.Adopt(button, false)
|
controlRow.Adopt(button)
|
||||||
button.Focus()
|
button.Focus()
|
||||||
} else {
|
} else {
|
||||||
var button *elements.Button
|
var button *elements.Button
|
||||||
@@ -74,7 +78,7 @@ func NewDialog (
|
|||||||
buttonDescriptor.OnPress()
|
buttonDescriptor.OnPress()
|
||||||
window.Close()
|
window.Close()
|
||||||
})
|
})
|
||||||
container.Adopt(button, false)
|
controlRow.Adopt(button)
|
||||||
}
|
}
|
||||||
button.Focus()
|
button.Focus()
|
||||||
}
|
}
|
||||||
|
|||||||
2
theme.go
2
theme.go
@@ -197,8 +197,8 @@ const (
|
|||||||
|
|
||||||
IconBackward
|
IconBackward
|
||||||
IconForward
|
IconForward
|
||||||
IconRefresh
|
|
||||||
IconUpward
|
IconUpward
|
||||||
|
IconRefresh
|
||||||
|
|
||||||
IconYes
|
IconYes
|
||||||
IconNo
|
IconNo
|
||||||
|
|||||||
6
tomo.go
6
tomo.go
@@ -29,6 +29,12 @@ func Do (callback func ()) {
|
|||||||
backend.Do(callback)
|
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
|
// 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
|
// MainWindow. If the window could not be created, an error is returned
|
||||||
// explaining why.
|
// explaining why.
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ type Window interface {
|
|||||||
// these at one time.
|
// these at one time.
|
||||||
Adopt (Element)
|
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
|
// SetTitle sets the title that appears on the window's title bar. This
|
||||||
// method might have no effect with some backends.
|
// method might have no effect with some backends.
|
||||||
SetTitle (string)
|
SetTitle (string)
|
||||||
|
|||||||
Reference in New Issue
Block a user