atomize-element-interface #2

Merged
sashakoshka merged 20 commits from atomize-element-interface into main 2023-01-16 17:24:23 +00:00
25 changed files with 731 additions and 662 deletions

View File

@ -16,6 +16,7 @@ type characterLayout struct {
type wordLayout struct {
position image.Point
width int
spaceAfter int
text []characterLayout
}
@ -160,6 +161,25 @@ func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) {
return
}
// ReccomendedHeightFor returns the reccomended max height if the text were to
// have its maximum width set to the given width. This does not alter the
// drawer's state.
func (drawer *TextDrawer) ReccomendedHeightFor (width int) (height int) {
if !drawer.layoutClean { drawer.recalculate() }
metrics := drawer.face.Metrics()
dot := fixed.Point26_6 { 0, 0 }
for _, word := range drawer.layout {
dot.X += fixed.Int26_6((word.width + word.spaceAfter) << 6)
if word.width + word.position.X > width && word.position.X > 0 {
dot.Y += metrics.Height
dot.X = fixed.Int26_6(word.width << 6)
}
}
return dot.Y.Round()
}
func (drawer *TextDrawer) recalculate () {
drawer.layoutClean = true
drawer.layout = nil
@ -219,9 +239,6 @@ func (drawer *TextDrawer) recalculate () {
dot.X = wordWidth
}
// add the word to the layout
drawer.layout = append(drawer.layout, word)
// skip over whitespace, going onto a new line if there is a
// newline character
for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
@ -233,6 +250,7 @@ func (drawer *TextDrawer) recalculate () {
index ++
} else {
_, advance, ok := drawer.face.GlyphBounds(character)
word.spaceAfter = advance.Round()
index ++
if !ok { continue }
@ -246,6 +264,9 @@ func (drawer *TextDrawer) recalculate () {
}
}
// add the word to the layout
drawer.layout = append(drawer.layout, word)
// if there is a set maximum height, and we have crossed it,
// stop processing more words. and remove any words that have
// also crossed the line.

View File

@ -27,6 +27,8 @@ func (window *Window) handleConfigureNotify (
connection *xgbutil.XUtil,
event xevent.ConfigureNotifyEvent,
) {
if window.child == nil { return }
configureEvent := *event.ConfigureNotifyEvent
newWidth := int(configureEvent.Width)
@ -66,33 +68,20 @@ func (window *Window) handleKeyPress (
NumberPad: numberPad,
}
keyDownEvent := tomo.EventKeyDown {
Key: key,
Modifiers: modifiers,
Repeated: false, // FIXME: return correct value here
}
if keyDownEvent.Key == tomo.KeyTab && keyDownEvent.Modifiers.Alt {
if window.child.Selectable() {
direction := 1
if keyDownEvent.Modifiers.Shift {
direction = -1
if key == tomo.KeyTab && modifiers.Alt {
if child, ok := window.child.(tomo.Selectable); ok {
direction := tomo.SelectionDirectionForward
if modifiers.Shift {
direction = tomo.SelectionDirectionBackward
}
window.advanceSelectionInChild(direction)
if !child.HandleSelection(direction) {
child.HandleDeselection()
}
}
} else {
window.child.Handle(keyDownEvent)
}
}
func (window *Window) advanceSelectionInChild (direction int) {
if window.child.Selected() {
if !window.child.AdvanceSelection(direction) {
window.child.Handle(tomo.EventDeselect { })
}
} else {
window.child.Handle(tomo.EventSelect { })
} else if child, ok := window.child.(tomo.KeyboardTarget); ok {
// FIXME: pass correct value for repeated
child.HandleKeyDown(key, modifiers, false)
}
}
@ -115,11 +104,10 @@ func (window *Window) handleKeyRelease (
Hyper: (keyEvent.State & window.backend.modifierMasks.hyper) > 0,
NumberPad: numberPad,
}
window.child.Handle (tomo.EventKeyUp {
Key: key,
Modifiers: modifiers,
})
if child, ok := window.child.(tomo.KeyboardTarget); ok {
child.HandleKeyUp(key, modifiers)
}
}
func (window *Window) handleButtonPress (
@ -128,48 +116,54 @@ func (window *Window) handleButtonPress (
) {
if window.child == nil { return }
buttonEvent := *event.ButtonPressEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
sum := scrollSum { }
sum.add(buttonEvent.Detail)
window.compressScrollSum(buttonEvent, &sum)
window.child.Handle (tomo.EventScroll {
X: int(buttonEvent.EventX),
Y: int(buttonEvent.EventY),
ScrollX: sum.x,
ScrollY: sum.y,
})
} else {
window.child.Handle (tomo.EventMouseDown {
Button: tomo.Button(buttonEvent.Detail),
X: int(buttonEvent.EventX),
Y: int(buttonEvent.EventY),
})
if child, ok := window.child.(tomo.MouseTarget); ok {
buttonEvent := *event.ButtonPressEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
sum := scrollSum { }
sum.add(buttonEvent.Detail)
window.compressScrollSum(buttonEvent, &sum)
child.HandleScroll (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
float64(sum.x), float64(sum.y))
} else {
child.HandleMouseDown (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
tomo.Button(buttonEvent.Detail))
}
}
}
func (window *Window) handleButtonRelease (
connection *xgbutil.XUtil,
event xevent.ButtonReleaseEvent,
) {
buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
window.child.Handle (tomo.EventMouseUp {
Button: tomo.Button(buttonEvent.Detail),
X: int(buttonEvent.EventX),
Y: int(buttonEvent.EventY),
})
if window.child == nil { return }
if child, ok := window.child.(tomo.MouseTarget); ok {
buttonEvent := *event.ButtonReleaseEvent
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
child.HandleMouseUp (
int(buttonEvent.EventX),
int(buttonEvent.EventY),
tomo.Button(buttonEvent.Detail))
}
}
func (window *Window) handleMotionNotify (
connection *xgbutil.XUtil,
event xevent.MotionNotifyEvent,
) {
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
window.child.Handle (tomo.EventMouseMove {
X: int(motionEvent.EventX),
Y: int(motionEvent.EventY),
})
if window.child == nil { return }
if child, ok := window.child.(tomo.MouseTarget); ok {
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
child.HandleMouseMove (
int(motionEvent.EventX),
int(motionEvent.EventY))
}
}

View File

@ -78,7 +78,11 @@ func (backend *Backend) NewWindow (
func (window *Window) Adopt (child tomo.Element) {
if window.child != nil {
child.SetParentHooks (tomo.ParentHooks { })
if child.Selected() { child.Handle(tomo.EventDeselect { }) }
if previousChild, ok := window.child.(tomo.Selectable); ok {
if previousChild.Selected() {
previousChild.HandleDeselection()
}
}
}
window.child = child
if child != nil {
@ -88,7 +92,6 @@ func (window *Window) Adopt (child tomo.Element) {
SelectionRequest: window.childSelectionRequestCallback,
})
if child.Selectable() { child.Handle(tomo.EventSelect { }) }
window.resizeChildToFit()
}
window.childMinimumSizeChangeCallback(child.MinimumSize())
@ -199,12 +202,18 @@ func (window *Window) redrawChildEntirely () {
func (window *Window) resizeChildToFit () {
window.skipChildDrawCallback = true
window.child.Handle(tomo.EventResize {
Width: window.metrics.width,
Height: window.metrics.height,
})
if child, ok := window.child.(tomo.Expanding); ok {
minimumHeight := child.MinimumHeightFor(window.metrics.width)
_, minimumWidth := child.MinimumSize()
window.childMinimumSizeChangeCallback (
minimumWidth, minimumHeight)
} else {
window.child.Resize (
window.metrics.width,
window.metrics.height)
window.redrawChildEntirely()
}
window.skipChildDrawCallback = false
window.redrawChildEntirely()
}
func (window *Window) childDrawCallback (region tomo.Canvas) {
@ -246,7 +255,9 @@ func (window *Window) childMinimumSizeChangeCallback (width, height int) {
}
func (window *Window) childSelectionRequestCallback () (granted bool) {
window.child.Handle(tomo.EventSelect { })
if child, ok := window.child.(tomo.Selectable); ok {
child.HandleSelection(tomo.SelectionDirectionNeutral)
}
return true
}

View File

@ -4,8 +4,9 @@ import "image"
import "image/draw"
import "image/color"
// Canvas is like Image but also requires Set and SetRGBA methods. This
// interface can be easily satisfied using an image.RGBA struct.
// Canvas is like draw.Image but is also able to return a raw pixel buffer for
// more efficient drawing. This interface can be easily satisfied using a
// BasicCanvas struct.
type Canvas interface {
draw.Image
Buffer () (data []color.RGBA, stride int)

175
element.go Normal file
View File

@ -0,0 +1,175 @@
package tomo
// ParentHooks is a struct that contains callbacks that let child elements send
// information to their parent element without the child element knowing
// anything about the parent element or containing any reference to it. When a
// parent element adopts a child element, it must set these callbacks.
type ParentHooks struct {
// Draw is called when a part of the child element's surface is updated.
// The updated region will be passed to the callback as a sub-image.
Draw func (region Canvas)
// MinimumSizeChange is called when the child element's minimum width
// and/or height changes. When this function is called, the element will
// have already been resized and there is no need to send it a resize
// event.
MinimumSizeChange func (width, height int)
// SelectionRequest is called when the child element element wants
// itself to be selected. If the parent element chooses to grant the
// request, it must send the child element a selection event and return
// true.
SelectionRequest func () (granted bool)
}
// RunDraw runs the Draw hook if it is not nil. If it is nil, it does nothing.
func (hooks ParentHooks) RunDraw (region Canvas) {
if hooks.Draw != nil {
hooks.Draw(region)
}
}
// RunMinimumSizeChange runs the MinimumSizeChange hook if it is not nil. If it
// is nil, it does nothing.
func (hooks ParentHooks) RunMinimumSizeChange (width, height int) {
if hooks.MinimumSizeChange != nil {
hooks.MinimumSizeChange(width, height)
}
}
// RunSelectionRequest runs the SelectionRequest hook if it is not nil. If it is
// nil, it does nothing.
func (hooks ParentHooks) RunSelectionRequest () (granted bool) {
if hooks.SelectionRequest != nil {
granted = hooks.SelectionRequest()
}
return
}
// Element represents a basic on-screen object.
type Element interface {
// Element must implement the Canvas interface. Elements should start
// out with a completely blank buffer, and only allocate memory and draw
// on it for the first time when sent an EventResize event.
Canvas
// MinimumSize specifies the minimum amount of pixels this element's
// width and height may be set to. If the element is given a resize
// event with dimensions smaller than this, it will use its minimum
// instead of the offending dimension(s).
MinimumSize () (width, height int)
// Resize resizes the element. This should only be called by the
// element's parent.
Resize (width, height int)
// SetParentHooks gives the element callbacks that let it send
// information to its parent element without it knowing anything about
// the parent element or containing any reference to it. When a parent
// element adopts a child element, it must set these callbacks.
SetParentHooks (callbacks ParentHooks)
}
// SelectionDirection represents a keyboard navigation direction.
type SelectionDirection int
const (
SelectionDirectionNeutral SelectionDirection = 0
SelectionDirectionBackward SelectionDirection = -1
SelectionDirectionForward SelectionDirection = 1
)
// Canon returns a well-formed direction.
func (direction SelectionDirection) Canon () (canon SelectionDirection) {
if direction > 0 {
return SelectionDirectionForward
} else if direction == 0 {
return SelectionDirectionNeutral
} else {
return SelectionDirectionBackward
}
}
// Selectable represents an element that has keyboard navigation support. This
// includes inputs, buttons, sliders, etc. as well as any elements that have
// children (so keyboard navigation events can be propagated downward).
type Selectable interface {
Element
// Selected returns whether or not this element is currently selected.
Selected () (selected bool)
// Select selects this element, if its parent element grants the
// request.
Select ()
// HandleSelection causes this element to mark itself as selected, if it
// can currently be. Otherwise, it will return false and do nothing.
HandleSelection (direction SelectionDirection) (accepted bool)
// HandleDeselection causes this element to mark itself and all of its
// children as deselected.
HandleDeselection ()
}
// KeyboardTarget represents an element that can receive keyboard input.
type KeyboardTarget interface {
Element
// HandleKeyDown is called when a key is pressed down 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.
HandleKeyDown (key Key, modifiers Modifiers, repeated bool)
// HandleKeyUp is called when a key is released while this element has
// keyboard focus.
HandleKeyUp (key Key, modifiers Modifiers)
}
// MouseTarget represents an element that can receive mouse events.
type MouseTarget interface {
Element
// Each of these handler methods is passed the position of the mouse
// cursor at the time of the event as x, y.
// HandleMouseDown is called when a mouse button is pressed down on this
// element.
HandleMouseDown (x, y int, button Button)
// HandleMouseUp is called when a mouse button is released that was
// originally pressed down on this element.
HandleMouseUp (x, y int, button Button)
// HandleMouseMove is called when the mouse is moved over this element,
// or the mouse is moving while being held down and originally pressed
// down on this element.
HandleMouseMove (x, y int)
// HandleScroll is called when the mouse is scrolled. The X and Y
// direction of the scroll event are passed as deltaX and deltaY.
HandleScroll (x, y int, deltaX, deltaY float64)
}
// Expanding represents an element who's preferred minimum height can change in
// response to its width.
type Expanding interface {
Element
// HeightForWidth returns what the element's minimum height would be if
// resized to a specified width. This does not actually alter the state
// of the element in any way, but it may perform significant work, so it
// should be called sparingly.
//
// It is reccomended that parent containers check for this interface and
// take this method's value into account in order to support things like
// flow layouts and text wrapping, but it is not absolutely necessary.
// The element's MinimumSize method will still return the absolute
// minimum size that the element may be resized to.
//
// It is important to note that if a parent container checks for
// expanding chilren, it itself will likely need to be expanding.
MinimumHeightFor (width int) (height int)
}

View File

@ -13,6 +13,7 @@ type Button struct {
pressed bool
enabled bool
selected bool
onClick func ()
text string
@ -24,89 +25,103 @@ func NewButton (text string) (element *Button) {
element = &Button { enabled: true }
element.Core, element.core = core.NewCore(element)
element.drawer.SetFace(theme.FontFaceRegular())
element.core.SetSelectable(true)
element.SetText(text)
return
}
// Handle handles an event.
func (element *Button) Handle (event tomo.Event) {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
element.core.AllocateCanvas (
resizeEvent.Width,
resizeEvent.Height)
element.draw()
func (element *Button) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
}
case tomo.EventMouseDown:
if !element.enabled { break }
func (element *Button) HandleMouseDown (x, y int, button tomo.Button) {
element.Select()
if button != tomo.ButtonLeft { return }
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
}
func (element *Button) HandleMouseUp (x, y int, button tomo.Button) {
if button != tomo.ButtonLeft { return }
element.pressed = false
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
within := image.Point { x, y }.
In(element.Bounds())
mouseDownEvent := event.(tomo.EventMouseDown)
element.Select()
if mouseDownEvent.Button != tomo.ButtonLeft { break }
if within && element.onClick != nil {
element.onClick()
}
}
func (element *Button) HandleMouseMove (x, y int) { }
func (element *Button) HandleScroll (x, y int, deltaX, deltaY float64) { }
func (element *Button) HandleKeyDown (
key tomo.Key,
modifiers tomo.Modifiers,
repeated bool,
) {
if key == tomo.KeyEnter {
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
}
}
case tomo.EventKeyDown:
keyDownEvent := event.(tomo.EventKeyDown)
if keyDownEvent.Key == tomo.KeyEnter {
element.pressed = true
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
}
case tomo.EventMouseUp:
if !element.enabled { break }
mouseUpEvent := event.(tomo.EventMouseUp)
if mouseUpEvent.Button != tomo.ButtonLeft { break }
func (element *Button) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) {
if key == tomo.KeyEnter && element.pressed {
element.pressed = false
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
within := image.Point { mouseUpEvent.X, mouseUpEvent.Y }.
In(element.Bounds())
if within && element.onClick != nil {
if element.onClick != nil {
element.onClick()
}
case tomo.EventKeyUp:
keyDownEvent := event.(tomo.EventKeyUp)
if keyDownEvent.Key == tomo.KeyEnter && element.pressed {
element.pressed = false
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
if element.onClick != nil {
element.onClick()
}
}
case tomo.EventSelect:
element.core.SetSelected(true)
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
case tomo.EventDeselect:
element.core.SetSelected(false)
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
}
return
}
func (element *Button) Selected () (selected bool) {
return element.selected
}
func (element *Button) Select () {
element.core.RequestSelection()
}
func (element *Button) HandleSelection (
direction tomo.SelectionDirection,
) (
accepted bool,
) {
if !element.enabled { return false }
if element.selected && direction != tomo.SelectionDirectionNeutral {
return false
}
element.selected = true
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
return true
}
func (element *Button) HandleDeselection () {
element.selected = false
if element.core.HasImage() {
element.draw()
element.core.PushAll()
}
}
// OnClick sets the function to be called when the button is clicked.
@ -114,17 +129,10 @@ func (element *Button) OnClick (callback func ()) {
element.onClick = callback
}
// Select requests that this button's parent container send it a selection
// event.
func (element *Button) Select () {
element.core.Select()
}
// 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.core.SetSelectable(enabled)
if element.core.HasImage () {
element.draw()
element.core.PushAll()

View File

@ -12,10 +12,12 @@ type Container struct {
*core.Core
core core.CoreControl
layout tomo.Layout
children []tomo.LayoutEntry
drags [10]tomo.Element
warping bool
layout tomo.Layout
children []tomo.LayoutEntry
drags [10]tomo.MouseTarget
warping bool
selected bool
selectable bool
}
// NewContainer creates a new container.
@ -44,18 +46,10 @@ func (element *Container) Adopt (child tomo.Element, expand bool) {
MinimumSizeChange: func (int, int) {
element.updateMinimumSize()
},
SelectabilityChange: func (bool) {
element.updateSelectable()
},
SelectionRequest: func () (granted bool) {
if !child.Selectable() { return }
if element.core.Select() {
element.propogateToSelected(tomo.EventDeselect { })
child.Handle(tomo.EventSelect { })
return true
}
return
child, selectable := child.(tomo.Selectable)
if !selectable { return }
return element.childSelectionRequestCallback(child)
},
Draw: func (region tomo.Canvas) {
element.drawChildRegion(child, region)
@ -176,122 +170,168 @@ func (element *Container) childPosition (child tomo.Element) (position image.Poi
return
}
func (element *Container) Handle (event tomo.Event) {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
element.core.AllocateCanvas (
resizeEvent.Width,
resizeEvent.Height)
element.recalculate()
element.draw()
case tomo.EventMouseDown:
mouseDownEvent := event.(tomo.EventMouseDown)
child := element.ChildAt (image.Pt (
mouseDownEvent.X,
mouseDownEvent.Y))
if child == nil { break }
element.drags[mouseDownEvent.Button] = child
childPosition := element.childPosition(child)
child.Handle (tomo.EventMouseDown {
Button: mouseDownEvent.Button,
X: mouseDownEvent.X - childPosition.X,
Y: mouseDownEvent.Y - childPosition.Y,
})
case tomo.EventMouseUp:
mouseUpEvent := event.(tomo.EventMouseUp)
child := element.drags[mouseUpEvent.Button]
if child == nil { break }
element.drags[mouseUpEvent.Button] = nil
childPosition := element.childPosition(child)
child.Handle (tomo.EventMouseUp {
Button: mouseUpEvent.Button,
X: mouseUpEvent.X - childPosition.X,
Y: mouseUpEvent.Y - childPosition.Y,
})
case tomo.EventMouseMove:
mouseMoveEvent := event.(tomo.EventMouseMove)
for _, child := range element.drags {
if child == nil { continue }
childPosition := element.childPosition(child)
child.Handle (tomo.EventMouseMove {
X: mouseMoveEvent.X - childPosition.X,
Y: mouseMoveEvent.Y - childPosition.Y,
})
}
case tomo.EventSelect:
if !element.Selectable() { break }
element.core.SetSelected(true)
// select the first selectable element
for _, entry := range element.children {
if entry.Selectable() {
entry.Handle(event)
break
}
}
case tomo.EventDeselect:
element.core.SetSelected(false)
element.propogateToSelected(event)
default:
// other events are just directly sent to the selected child.
element.propogateToSelected(event)
}
return
func (element *Container) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.recalculate()
element.draw()
}
func (element *Container) propogateToSelected (event tomo.Event) {
for _, entry := range element.children {
if entry.Selected() {
entry.Handle(event)
}
// TODO: implement KeyboardTarget
func (element *Container) HandleMouseDown (x, y int, button tomo.Button) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget)
if !handlesMouse { return }
element.drags[button] = child
childPosition := element.childPosition(child)
child.HandleMouseDown(x - childPosition.X, y - childPosition.Y, button)
}
func (element *Container) HandleMouseUp (x, y int, button tomo.Button) {
child := element.drags[button]
if child == nil { return }
element.drags[button] = nil
childPosition := element.childPosition(child)
child.HandleMouseUp(x - childPosition.X, y - childPosition.Y, button)
}
func (element *Container) HandleMouseMove (x, y int) {
for _, child := range element.drags {
if child == nil { continue }
childPosition := element.childPosition(child)
child.HandleMouseMove(x - childPosition.X, y - childPosition.Y)
}
}
func (element *Container) AdvanceSelection (direction int) (ok bool) {
if !element.Selectable() { return }
func (element *Container) HandleScroll (x, y int, deltaX, deltaY float64) {
child, handlesMouse := element.ChildAt(image.Pt(x, y)).(tomo.MouseTarget)
if !handlesMouse { return }
childPosition := element.childPosition(child)
child.HandleScroll(x - childPosition.X, y - childPosition.Y, deltaX, deltaY)
}
func (element *Container) HandleKeyDown (
key tomo.Key,
modifiers tomo.Modifiers,
repeated bool,
) {
element.forSelected (func (child tomo.Selectable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyDown(key, modifiers, repeated)
}
return true
})
}
func (element *Container) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) {
element.forSelected (func (child tomo.Selectable) bool {
child0, handlesKeyboard := child.(tomo.KeyboardTarget)
if handlesKeyboard {
child0.HandleKeyUp(key, modifiers)
}
return true
})
}
func (element *Container) Selected () (selected bool) {
return element.selected
}
func (element *Container) Select () {
element.core.RequestSelection()
}
func (element *Container) HandleSelection (direction tomo.SelectionDirection) (ok bool) {
if !element.selectable { return false }
direction = direction.Canon()
firstSelected := element.firstSelected()
if firstSelected < 0 {
for _, entry := range element.children {
if entry.Selectable() {
entry.Handle(tomo.EventSelect { })
found := false
switch direction {
case tomo.SelectionDirectionBackward:
element.forSelectableBackward (func (child tomo.Selectable) bool {
if child.HandleSelection(direction) {
element.selected = true
found = true
return false
}
return true
})
return true
case tomo.SelectionDirectionNeutral, tomo.SelectionDirectionForward:
element.forSelectable (func (child tomo.Selectable) bool {
if child.HandleSelection(direction) {
element.selected = true
found = true
return false
}
return true
})
}
return found
} else {
firstSelectedChild :=
element.children[firstSelected].Element.(tomo.Selectable)
for index := firstSelected + int(direction);
index < len(element.children) && index >= 0;
index += int(direction) {
child, selectable :=
element.children[index].
Element.(tomo.Selectable)
if selectable && child.HandleSelection(direction) {
firstSelectedChild.HandleDeselection()
element.selected = true
return true
}
}
} else {
nextSelectable := -1
step := 1
if direction < 0 { step = - 1 }
for index := firstSelected + step;
index < len(element.children) && index > 0;
index += step {
if element.children[index].Selectable() {
nextSelectable = index
break
}
}
if nextSelectable > 0 {
element.children[firstSelected ].Handle(tomo.EventDeselect { })
element.children[nextSelectable].Handle(tomo.EventSelect { })
return true
}
}
return
return false
}
func (element *Container) HandleDeselection () {
element.selected = false
element.forSelected (func (child tomo.Selectable) bool {
child.HandleDeselection()
return true
})
}
func (element *Container) forSelected (callback func (child tomo.Selectable) bool) {
for _, entry := range element.children {
child, selectable := entry.Element.(tomo.Selectable)
if selectable && child.Selected() {
if !callback(child) { break }
}
}
}
func (element *Container) forSelectable (callback func (child tomo.Selectable) bool) {
for _, entry := range element.children {
child, selectable := entry.Element.(tomo.Selectable)
if selectable {
if !callback(child) { break }
}
}
}
func (element *Container) forSelectableBackward (callback func (child tomo.Selectable) bool) {
for index := len(element.children) - 1; index >= 0; index -- {
child, selectable := element.children[index].Element.(tomo.Selectable)
if selectable {
if !callback(child) { break }
}
}
}
func (element *Container) firstSelected () (index int) {
for currentIndex, entry := range element.children {
if entry.Selected() {
child, selectable := entry.Element.(tomo.Selectable)
if selectable && child.Selected() {
return currentIndex
}
}
@ -299,15 +339,36 @@ func (element *Container) firstSelected () (index int) {
}
func (element *Container) updateSelectable () {
selectable := false
for _, entry := range element.children {
if entry.Selectable() { selectable = true }
element.selectable = false
element.forSelectable (func (tomo.Selectable) bool {
element.selectable = true
return false
})
if !element.selectable {
element.selected = false
}
}
func (element *Container) childSelectionRequestCallback (
child tomo.Selectable,
) (
granted bool,
) {
if element.core.RequestSelection() {
element.forSelected (func (child tomo.Selectable) bool {
child.HandleDeselection()
return true
})
child.HandleSelection(tomo.SelectionDirectionNeutral)
return true
} else {
return false
}
element.core.SetSelectable(selectable)
}
func (element *Container) updateMinimumSize () {
element.core.SetMinimumSize(element.layout.MinimumSize(element.children))
element.core.SetMinimumSize (
element.layout.MinimumSize(element.children, 1e9))
}
func (element *Container) recalculate () {

View File

@ -1,7 +1,6 @@
package basic
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
@ -28,20 +27,13 @@ func NewLabel (text string, wrap bool) (element *Label) {
return
}
// Handle handles and event.
func (element *Label) Handle (event tomo.Event) {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
element.core.AllocateCanvas (
resizeEvent.Width,
resizeEvent.Height)
if element.wrap {
element.drawer.SetMaxWidth (resizeEvent.Width)
element.drawer.SetMaxHeight(resizeEvent.Height)
}
element.draw()
func (element *Label) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
if element.wrap {
element.drawer.SetMaxWidth(width)
element.drawer.SetMaxHeight(height)
}
element.draw()
return
}

View File

@ -27,89 +27,87 @@ func NewCore (parent tomo.Element) (core *Core, control CoreControl) {
return
}
func (core Core) ColorModel () (model color.Model) {
// ColorModel fulfills the draw.Image interface.
func (core *Core) ColorModel () (model color.Model) {
return color.RGBAModel
}
func (core Core) At (x, y int) (pixel color.Color) {
// ColorModel fulfills the draw.Image interface.
func (core *Core) At (x, y int) (pixel color.Color) {
return core.canvas.At(x, y)
}
func (core Core) Bounds () (bounds image.Rectangle) {
// ColorModel fulfills the draw.Image interface.
func (core *Core) Bounds () (bounds image.Rectangle) {
return core.canvas.Bounds()
}
func (core Core) Set (x, y int, c color.Color) () {
// ColorModel fulfills the draw.Image interface.
func (core *Core) Set (x, y int, c color.Color) () {
core.canvas.Set(x, y, c)
}
func (core Core) Buffer () (data []color.RGBA, stride int) {
// Buffer fulfills the tomo.Canvas interface.
func (core *Core) Buffer () (data []color.RGBA, stride int) {
return core.canvas.Buffer()
}
func (core Core) Selectable () (selectable bool) {
return core.selectable
}
func (core Core) Selected () (selected bool) {
return core.selected
}
func (core Core) AdvanceSelection (direction int) (ok bool) {
return
// 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
}
// SetParentHooks fulfils the tomo.Element interface. This should not need to be
// overridden.
func (core *Core) SetParentHooks (hooks tomo.ParentHooks) {
core.hooks = hooks
}
func (core Core) MinimumSize () (width, height int) {
return core.metrics.minimumWidth, core.metrics.minimumHeight
}
// CoreControl is a struct that can exert control over a control struct. It can
// be used as a canvas. It must not be directly embedded into an element, but
// instead kept as a private member.
// 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 {
tomo.BasicCanvas
core *Core
}
func (control CoreControl) HasImage () (empty bool) {
return !control.Bounds().Empty()
}
func (control CoreControl) Select () (granted bool) {
// RequestSelection requests that the element's parent send it a selection
// event. If the request was granted, it returns true. If it was denied, it
// returns false.
func (control CoreControl) RequestSelection () (granted bool) {
return control.core.hooks.RunSelectionRequest()
}
func (control CoreControl) SetSelected (selected bool) {
if !control.core.selectable { return }
control.core.selected = selected
}
func (control CoreControl) SetSelectable (selectable bool) {
if control.core.selectable == selectable { return }
control.core.selectable = selectable
if !selectable { control.core.selected = false }
control.core.hooks.RunSelectabilityChange(selectable)
// 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.Bounds().Empty()
}
// PushRegion 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) PushRegion (bounds image.Rectangle) {
control.core.hooks.RunDraw(tomo.Cut(control, bounds))
}
// PushAll pushes all pixels to the parent element. This does not need to be
// called when responding to a resize event.
func (control CoreControl) PushAll () {
control.PushRegion(control.Bounds())
}
// AllocateCanvas resizes the canvas, constraining the width and height so that
// they are not less than the specified minimum width and height.
func (control *CoreControl) AllocateCanvas (width, height int) {
core := control.core
width, height, _ = control.ConstrainSize(width, height)
core.canvas = tomo.NewBasicCanvas(width, height)
control.BasicCanvas = core.canvas
control.core.canvas = tomo.NewBasicCanvas(width, height)
control.BasicCanvas = control.core.canvas
}
// 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 &&
@ -123,20 +121,19 @@ func (control CoreControl) SetMinimumSize (width, height int) {
// if there is an image buffer, and the current size is less
// than this new minimum size, send core.parent a resize event.
bounds := control.Bounds()
imageWidth,
imageHeight,
constrained := control.ConstrainSize (
bounds.Dx(),
bounds.Dy())
if constrained {
core.parent.Handle (tomo.EventResize {
Width: imageWidth,
Height: imageHeight,
})
if control.HasImage() {
bounds := control.Bounds()
imageWidth,
imageHeight,
constrained := control.ConstrainSize(bounds.Dx(), bounds.Dy())
if constrained {
core.parent.Resize(imageWidth, imageHeight)
}
}
}
// 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,
) (

View File

@ -3,7 +3,6 @@ package fun
import "time"
import "math"
import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
import "git.tebibyte.media/sashakoshka/tomo/artist"
import "git.tebibyte.media/sashakoshka/tomo/elements/core"
@ -21,15 +20,9 @@ func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
return
}
func (element *AnalogClock) Handle (event tomo.Event) {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
element.core.AllocateCanvas (
resizeEvent.Width,
resizeEvent.Height)
element.draw()
}
func (element *AnalogClock) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
element.draw()
}
func (element *AnalogClock) SetTime (newTime time.Time) {

View File

@ -26,58 +26,47 @@ func NewMouse () (element *Mouse) {
return
}
func (element *Mouse) Handle (event tomo.Event) {
switch event.(type) {
case tomo.EventResize:
resizeEvent := event.(tomo.EventResize)
element.core.AllocateCanvas (
resizeEvent.Width,
resizeEvent.Height)
artist.FillRectangle (
element.core,
theme.AccentPattern(),
element.Bounds())
artist.StrokeRectangle (
element.core,
artist.NewUniform(color.Black), 1,
element.Bounds())
artist.Line (
element.core, artist.NewUniform(color.White), 3,
image.Pt(1, 1),
image.Pt(resizeEvent.Width - 2, resizeEvent.Height - 2))
artist.Line (
element.core, artist.NewUniform(color.White), 1,
image.Pt(1, resizeEvent.Height - 2),
image.Pt(resizeEvent.Width - 2, 1))
case tomo.EventMouseDown:
element.drawing = true
mouseDownEvent := event.(tomo.EventMouseDown)
element.lastMousePos = image.Pt (
mouseDownEvent.X,
mouseDownEvent.Y)
case tomo.EventMouseUp:
element.drawing = false
mouseUpEvent := event.(tomo.EventMouseUp)
mousePos := image.Pt (
mouseUpEvent.X,
mouseUpEvent.Y)
element.core.PushRegion (artist.Line (
element.core, element.color, 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
case tomo.EventMouseMove:
if !element.drawing { return }
mouseMoveEvent := event.(tomo.EventMouseMove)
mousePos := image.Pt (
mouseMoveEvent.X,
mouseMoveEvent.Y)
element.core.PushRegion (artist.Line (
element.core, element.color, 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
}
return
func (element *Mouse) Resize (width, height int) {
element.core.AllocateCanvas(width, height)
artist.FillRectangle (
element.core,
theme.AccentPattern(),
element.Bounds())
artist.StrokeRectangle (
element.core,
artist.NewUniform(color.Black), 1,
element.Bounds())
artist.Line (
element.core, artist.NewUniform(color.White), 3,
image.Pt(1, 1),
image.Pt(width - 2, height - 2))
artist.Line (
element.core, artist.NewUniform(color.White), 1,
image.Pt(1, height - 2),
image.Pt(width - 2, 1))
}
func (element *Mouse) HandleMouseDown (x, y int, button tomo.Button) {
element.drawing = true
element.lastMousePos = image.Pt(x, y)
}
func (element *Mouse) HandleMouseUp (x, y int, button tomo.Button) {
element.drawing = false
mousePos := image.Pt(x, y)
element.core.PushRegion (artist.Line (
element.core, element.color, 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
}
func (element *Mouse) HandleMouseMove (x, y int) {
if !element.drawing { return }
mousePos := image.Pt(x, y)
element.core.PushRegion (artist.Line (
element.core, element.color, 1,
element.lastMousePos, mousePos))
element.lastMousePos = mousePos
}
func (element *Mouse) HandleScroll (x, y int, deltaX, deltaY float64) { }

View File

@ -1,88 +0,0 @@
package tomo
// Event represents any event. Use a type switch to figure out what sort of
// event it is.
type Event interface { }
// EventResize is sent to an element when its parent decides to resize it.
// Elements should not do anything if the width and height do not change.
type EventResize struct {
// The width and height the element should not be less than the
// element's reported minimum width and height. If by some chance they
// are anyways, the element should use its minimum width and height
// instead.
Width, Height int
}
// EventKeyDown is sent to the currently selected element when a key on the
// keyboard is pressed. Containers must propagate this event downwards.
type EventKeyDown struct {
Key
Modifiers
Repeated bool
}
// EventKeyDown is sent to the currently selected element when a key on the
// keyboard is released. Containers must propagate this event downwards.
type EventKeyUp struct {
Key
Modifiers
}
// EventMouseDown is sent to the element the mouse is positioned over when it is
// clicked. Containers must propagate this event downwards, with X and Y values
// relative to the top left corner of the child element.
type EventMouseDown struct {
// The button that was released
Button
// The X and Y position of the mouse cursor at the time of the event,
// relative to the top left corner of the element
X, Y int
}
// EventMouseUp is sent to the element that was positioned under the mouse the
// last time this particular mouse button was pressed down when it is released.
// Containers must propagate this event downwards, with X and Y values relative
// to the top left corner of the child element.
type EventMouseUp struct {
// The button that was released
Button
// The X and Y position of the mouse cursor at the time of the event,
// relative to the top left corner of the element
X, Y int
}
// EventMouseMove is sent to the element positioned under the mouse cursor when
// the mouse moves, or if a mouse button is currently being pressed, the element
// that the mouse was positioned under when it was pressed down. Containers must
// propogate this event downwards, with X and Y values relative to the top left
// corner of the child element.
type EventMouseMove struct {
// The X and Y position of the mouse cursor at the time of the event,
// relative to the top left corner of the element
X, Y int
}
// EventScroll is sent to the element positioned under the mouse cursor when the
// scroll wheel (or equivalent) is spun. Containers must propogate this event
// downwards.
type EventScroll struct {
// The X and Y position of the mouse cursor at the time of the event,
// relative to the top left corner of the element
X, Y int
// The X and Y amount the scroll wheel moved
ScrollX, ScrollY int
}
// EventSelect is sent to selectable elements when they become selected, whether
// by a mouse click or by keyboard navigation. Containers must propagate this
// event downwards.
type EventSelect struct { }
// EventDeselect is sent to selectable elements when they stop being selected,
// whether by a mouse click or by keyboard navigation. Containers must propagate
// this event downwards.
type EventDeselect struct { }

View File

@ -1,8 +1,8 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {

View File

@ -2,8 +2,8 @@ package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/flow"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {

View File

@ -3,9 +3,9 @@ package main
import "os"
import "time"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {

View File

@ -1,9 +1,9 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {

View File

@ -2,8 +2,8 @@ package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/popups"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {

View File

@ -1,9 +1,9 @@
package main
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
func main () {

27
layout.go Normal file
View File

@ -0,0 +1,27 @@
package tomo
import "image"
// LayoutEntry associates an element with layout and positioning information so
// it can be arranged by a Layout.
type LayoutEntry struct {
Element
Position image.Point
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, width, height int)
// MinimumSize returns the minimum width and height that the layout
// needs to properly arrange the given slice of layout entries, given a
// "suqeeze" width so that the height can be determined for elements
// fulfilling the Expanding interface.
MinimumSize (entries []LayoutEntry, squeeze int) (width, height int)
}

View File

@ -4,6 +4,11 @@ import "image"
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/theme"
// 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 Gap is true, a gap will be placed between each element.
Gap bool
@ -39,10 +44,7 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, width, height int) {
mainBounds := entries[0].Bounds()
if mainBounds.Dy() != mainHeight ||
mainBounds.Dx() != width {
entries[0].Handle (tomo.EventResize {
Width: width,
Height: mainHeight,
})
entries[0].Resize(width, mainHeight)
}
}
@ -94,10 +96,7 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, width, height int) {
entryBounds := entry.Bounds()
if entryBounds.Dy() != controlRowHeight ||
entryBounds.Dx() != entryWidth {
entry.Handle (tomo.EventResize {
Width: entryWidth,
Height: controlRowHeight,
})
entry.Resize(entryWidth, controlRowHeight)
}
}
}
@ -107,7 +106,12 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, width, height int) {
// 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) (width, height int) {
func (layout Dialog) MinimumSize (
entries []tomo.LayoutEntry,
squeeze int,
) (
width, height int,
) {
if len(entries) > 0 {
mainChildHeight := 0
width, mainChildHeight = entries[0].MinimumSize()

View File

@ -63,17 +63,19 @@ func (layout Horizontal) Arrange (entries []tomo.LayoutEntry, width, height int)
x += entryWidth
entryBounds := entry.Bounds()
if entryBounds.Dy() != height || entryBounds.Dx() != entryWidth {
entry.Handle (tomo.EventResize {
Width: entryWidth,
Height: height,
})
entry.Resize(entryWidth, height)
}
}
}
// 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) (width, height int) {
func (layout Horizontal) MinimumSize (
entries []tomo.LayoutEntry,
squeeze int,
) (
width, height int,
) {
for index, entry := range entries {
entryWidth, entryHeight := entry.MinimumSize()
if entryHeight > height {

View File

@ -63,18 +63,19 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
y += entryHeight
entryBounds := entry.Bounds()
if entryBounds.Dx() != width || entryBounds.Dy() != entryHeight {
// println(entryHeight)
entry.Handle (tomo.EventResize {
Width: width,
Height: entryHeight,
})
entry.Resize(width, entryHeight)
}
}
}
// 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) (width, height int) {
func (layout Vertical) MinimumSize (
entries []tomo.LayoutEntry,
squeeze int,
) (
width, height int,
) {
for index, entry := range entries {
entryWidth, entryHeight := entry.MinimumSize()
if entryWidth > width {

View File

@ -1,8 +1,8 @@
package popups
import "git.tebibyte.media/sashakoshka/tomo"
import "git.tebibyte.media/sashakoshka/tomo/layouts"
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
// DialogKind defines the semantic role of a dialog window.
type DialogKind int

158
tomo.go
View File

@ -1,165 +1,7 @@
package tomo
import "image"
import "errors"
// ParentHooks is a struct that contains callbacks that let child elements send
// information to their parent element without the child element knowing
// anything about the parent element or containing any reference to it. When a
// parent element adopts a child element, it must set these callbacks.
type ParentHooks struct {
// Draw is called when a part of the child element's surface is updated.
// The updated region will be passed to the callback as a sub-image.
Draw func (region Canvas)
// MinimumSizeChange is called when the child element's minimum width
// and/or height changes. When this function is called, the element will
// have already been resized and there is no need to send it a resize
// event.
MinimumSizeChange func (width, height int)
// SelectabilityChange is called when the chid element becomes
// selectable or non-selectable.
SelectabilityChange func (selectable bool)
// SelectionRequest is called when the child element element wants
// itself to be selected. If the parent element chooses to grant the
// request, it must send the child element a selection event and return
// true.
SelectionRequest func () (granted bool)
}
// RunDraw runs the Draw hook if it is not nil. If it is nil, it does nothing.
func (hooks ParentHooks) RunDraw (region Canvas) {
if hooks.Draw != nil {
hooks.Draw(region)
}
}
// RunMinimumSizeChange runs the MinimumSizeChange hook if it is not nil. If it
// is nil, it does nothing.
func (hooks ParentHooks) RunMinimumSizeChange (width, height int) {
if hooks.MinimumSizeChange != nil {
hooks.MinimumSizeChange(width, height)
}
}
// RunSelectionRequest runs the SelectionRequest hook if it is not nil. If it is
// nil, it does nothing.
func (hooks ParentHooks) RunSelectionRequest () (granted bool) {
if hooks.SelectionRequest != nil {
granted = hooks.SelectionRequest()
}
return
}
// RunSelectabilityChange runs the SelectionRequest hook if it is not nil. If it
// is nil, it does nothing.
func (hooks ParentHooks) RunSelectabilityChange (selectable bool) {
if hooks.SelectabilityChange != nil {
hooks.SelectabilityChange(selectable)
}
}
// Element represents a basic on-screen object.
type Element interface {
// Element must implement the Canvas interface. Elements should start
// out with a completely blank buffer, and only allocate memory and draw
// on it for the first time when sent an EventResize event.
Canvas
// Handle handles an event, propagating it to children if necessary.
Handle (event Event)
// Selectable returns whether this element can be selected. If this
// element contains other selectable elements, it must return true.
Selectable () (selectable bool)
// Selected returns whether or not this element is currently selected.
// This will always return false if it is not selectable.
Selected () (selected bool)
// If this element contains other elements, and one is selected, this
// method will advance the selection in the specified direction. If
// the element contains selectable elements but none of them are
// selected, it will select the first selectable element. If there are
// no more children to be selected in the specified direction, the
// element will return false. If the selection could be advanced, it
// will return true. If the element contains no selectable child
// elements, it will always return false.
AdvanceSelection (direction int) (ok bool)
// SetParentHooks gives the element callbacks that let it send
// information to its parent element without it knowing anything about
// the parent element or containing any reference to it. When a parent
// element adopts a child element, it must set these callbacks.
SetParentHooks (callbacks ParentHooks)
// MinimumSize specifies the minimum amount of pixels this element's
// width and height may be set to. If the element is given a resize
// event with dimensions smaller than this, it will use its minimum
// instead of the offending dimension(s).
MinimumSize () (width, height int)
}
// Window represents a top-level container generated by the currently running
// backend. It can contain a single element. It is hidden by default, and must
// be explicitly shown with the Show() method. If it contains no element, it
// displays a black (or transprent) background.
type Window interface {
// Adopt sets the root element of the window. There can only be one of
// these at one time.
Adopt (child Element)
// Child returns the root element of the window.
Child () (child Element)
// SetTitle sets the title that appears on the window's title bar. This
// method might have no effect with some backends.
SetTitle (title string)
// SetIcon taks in a list different sizes of the same icon and selects
// the best one to display on the window title bar, dock, or whatever is
// applicable for the given backend. This method might have no effect
// for some backends.
SetIcon (sizes []image.Image)
// Show shows the window. The window starts off hidden, so this must be
// called after initial setup to make sure it is visible.
Show ()
// Hide hides the window.
Hide ()
// Close closes the window.
Close ()
// OnClose specifies a function to be called when the window is closed.
OnClose (func ())
}
// LayoutEntry associates an element with layout and positioning information so
// it can be arranged by a Layout.
type LayoutEntry struct {
Element
Position image.Point
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, width, height int)
// MinimumSize returns the minimum width and height that the layout
// needs to properly arrange the given slice of layout entries.
MinimumSize (entries []LayoutEntry) (width, height int)
}
var backend Backend
// Run initializes a backend, calls the callback function, and begins the event

39
window.go Normal file
View File

@ -0,0 +1,39 @@
package tomo
import "image"
// Window represents a top-level container generated by the currently running
// backend. It can contain a single element. It is hidden by default, and must
// be explicitly shown with the Show() method. If it contains no element, it
// displays a black (or transprent) background.
type Window interface {
// Adopt sets the root element of the window. There can only be one of
// these at one time.
Adopt (child Element)
// Child returns the root element of the window.
Child () (child Element)
// SetTitle sets the title that appears on the window's title bar. This
// method might have no effect with some backends.
SetTitle (title string)
// SetIcon taks in a list different sizes of the same icon and selects
// the best one to display on the window title bar, dock, or whatever is
// applicable for the given backend. This method might have no effect
// for some backends.
SetIcon (sizes []image.Image)
// Show shows the window. The window starts off hidden, so this must be
// called after initial setup to make sure it is visible.
Show ()
// Hide hides the window.
Hide ()
// Close closes the window.
Close ()
// OnClose specifies a function to be called when the window is closed.
OnClose (func ())
}