Merge pull request 'atomize-element-interface' (#2) from atomize-element-interface into main
Reviewed-on: sashakoshka/tomo#2
This commit is contained in:
commit
71730dd70c
@ -16,6 +16,7 @@ type characterLayout struct {
|
|||||||
type wordLayout struct {
|
type wordLayout struct {
|
||||||
position image.Point
|
position image.Point
|
||||||
width int
|
width int
|
||||||
|
spaceAfter int
|
||||||
text []characterLayout
|
text []characterLayout
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,6 +161,25 @@ func (drawer *TextDrawer) LineHeight () (height fixed.Int26_6) {
|
|||||||
return
|
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 () {
|
func (drawer *TextDrawer) recalculate () {
|
||||||
drawer.layoutClean = true
|
drawer.layoutClean = true
|
||||||
drawer.layout = nil
|
drawer.layout = nil
|
||||||
@ -219,9 +239,6 @@ func (drawer *TextDrawer) recalculate () {
|
|||||||
dot.X = wordWidth
|
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
|
// skip over whitespace, going onto a new line if there is a
|
||||||
// newline character
|
// newline character
|
||||||
for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
|
for index < len(drawer.runes) && unicode.IsSpace(drawer.runes[index]) {
|
||||||
@ -233,6 +250,7 @@ func (drawer *TextDrawer) recalculate () {
|
|||||||
index ++
|
index ++
|
||||||
} else {
|
} else {
|
||||||
_, advance, ok := drawer.face.GlyphBounds(character)
|
_, advance, ok := drawer.face.GlyphBounds(character)
|
||||||
|
word.spaceAfter = advance.Round()
|
||||||
index ++
|
index ++
|
||||||
if !ok { continue }
|
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,
|
// if there is a set maximum height, and we have crossed it,
|
||||||
// stop processing more words. and remove any words that have
|
// stop processing more words. and remove any words that have
|
||||||
// also crossed the line.
|
// also crossed the line.
|
||||||
|
@ -27,6 +27,8 @@ func (window *Window) handleConfigureNotify (
|
|||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.ConfigureNotifyEvent,
|
event xevent.ConfigureNotifyEvent,
|
||||||
) {
|
) {
|
||||||
|
if window.child == nil { return }
|
||||||
|
|
||||||
configureEvent := *event.ConfigureNotifyEvent
|
configureEvent := *event.ConfigureNotifyEvent
|
||||||
|
|
||||||
newWidth := int(configureEvent.Width)
|
newWidth := int(configureEvent.Width)
|
||||||
@ -66,33 +68,20 @@ func (window *Window) handleKeyPress (
|
|||||||
NumberPad: numberPad,
|
NumberPad: numberPad,
|
||||||
}
|
}
|
||||||
|
|
||||||
keyDownEvent := tomo.EventKeyDown {
|
if key == tomo.KeyTab && modifiers.Alt {
|
||||||
Key: key,
|
if child, ok := window.child.(tomo.Selectable); ok {
|
||||||
Modifiers: modifiers,
|
direction := tomo.SelectionDirectionForward
|
||||||
Repeated: false, // FIXME: return correct value here
|
if modifiers.Shift {
|
||||||
}
|
direction = tomo.SelectionDirectionBackward
|
||||||
|
|
||||||
if keyDownEvent.Key == tomo.KeyTab && keyDownEvent.Modifiers.Alt {
|
|
||||||
if window.child.Selectable() {
|
|
||||||
direction := 1
|
|
||||||
if keyDownEvent.Modifiers.Shift {
|
|
||||||
direction = -1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
window.advanceSelectionInChild(direction)
|
if !child.HandleSelection(direction) {
|
||||||
|
child.HandleDeselection()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else if child, ok := window.child.(tomo.KeyboardTarget); ok {
|
||||||
window.child.Handle(keyDownEvent)
|
// FIXME: pass correct value for repeated
|
||||||
}
|
child.HandleKeyDown(key, modifiers, false)
|
||||||
}
|
|
||||||
|
|
||||||
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 { })
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -116,10 +105,9 @@ func (window *Window) handleKeyRelease (
|
|||||||
NumberPad: numberPad,
|
NumberPad: numberPad,
|
||||||
}
|
}
|
||||||
|
|
||||||
window.child.Handle (tomo.EventKeyUp {
|
if child, ok := window.child.(tomo.KeyboardTarget); ok {
|
||||||
Key: key,
|
child.HandleKeyUp(key, modifiers)
|
||||||
Modifiers: modifiers,
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) handleButtonPress (
|
func (window *Window) handleButtonPress (
|
||||||
@ -128,48 +116,54 @@ func (window *Window) handleButtonPress (
|
|||||||
) {
|
) {
|
||||||
if window.child == nil { return }
|
if window.child == nil { return }
|
||||||
|
|
||||||
buttonEvent := *event.ButtonPressEvent
|
if child, ok := window.child.(tomo.MouseTarget); ok {
|
||||||
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
|
buttonEvent := *event.ButtonPressEvent
|
||||||
sum := scrollSum { }
|
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 {
|
||||||
sum.add(buttonEvent.Detail)
|
sum := scrollSum { }
|
||||||
window.compressScrollSum(buttonEvent, &sum)
|
sum.add(buttonEvent.Detail)
|
||||||
window.child.Handle (tomo.EventScroll {
|
window.compressScrollSum(buttonEvent, &sum)
|
||||||
X: int(buttonEvent.EventX),
|
child.HandleScroll (
|
||||||
Y: int(buttonEvent.EventY),
|
int(buttonEvent.EventX),
|
||||||
ScrollX: sum.x,
|
int(buttonEvent.EventY),
|
||||||
ScrollY: sum.y,
|
float64(sum.x), float64(sum.y))
|
||||||
})
|
} else {
|
||||||
} else {
|
child.HandleMouseDown (
|
||||||
window.child.Handle (tomo.EventMouseDown {
|
int(buttonEvent.EventX),
|
||||||
Button: tomo.Button(buttonEvent.Detail),
|
int(buttonEvent.EventY),
|
||||||
X: int(buttonEvent.EventX),
|
tomo.Button(buttonEvent.Detail))
|
||||||
Y: int(buttonEvent.EventY),
|
}
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) handleButtonRelease (
|
func (window *Window) handleButtonRelease (
|
||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.ButtonReleaseEvent,
|
event xevent.ButtonReleaseEvent,
|
||||||
) {
|
) {
|
||||||
buttonEvent := *event.ButtonReleaseEvent
|
if window.child == nil { return }
|
||||||
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
|
|
||||||
window.child.Handle (tomo.EventMouseUp {
|
if child, ok := window.child.(tomo.MouseTarget); ok {
|
||||||
Button: tomo.Button(buttonEvent.Detail),
|
buttonEvent := *event.ButtonReleaseEvent
|
||||||
X: int(buttonEvent.EventX),
|
if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return }
|
||||||
Y: int(buttonEvent.EventY),
|
child.HandleMouseUp (
|
||||||
})
|
int(buttonEvent.EventX),
|
||||||
|
int(buttonEvent.EventY),
|
||||||
|
tomo.Button(buttonEvent.Detail))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) handleMotionNotify (
|
func (window *Window) handleMotionNotify (
|
||||||
connection *xgbutil.XUtil,
|
connection *xgbutil.XUtil,
|
||||||
event xevent.MotionNotifyEvent,
|
event xevent.MotionNotifyEvent,
|
||||||
) {
|
) {
|
||||||
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
if window.child == nil { return }
|
||||||
window.child.Handle (tomo.EventMouseMove {
|
|
||||||
X: int(motionEvent.EventX),
|
if child, ok := window.child.(tomo.MouseTarget); ok {
|
||||||
Y: int(motionEvent.EventY),
|
motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent)
|
||||||
})
|
child.HandleMouseMove (
|
||||||
|
int(motionEvent.EventX),
|
||||||
|
int(motionEvent.EventY))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -78,7 +78,11 @@ func (backend *Backend) NewWindow (
|
|||||||
func (window *Window) Adopt (child tomo.Element) {
|
func (window *Window) Adopt (child tomo.Element) {
|
||||||
if window.child != nil {
|
if window.child != nil {
|
||||||
child.SetParentHooks (tomo.ParentHooks { })
|
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
|
window.child = child
|
||||||
if child != nil {
|
if child != nil {
|
||||||
@ -88,7 +92,6 @@ func (window *Window) Adopt (child tomo.Element) {
|
|||||||
SelectionRequest: window.childSelectionRequestCallback,
|
SelectionRequest: window.childSelectionRequestCallback,
|
||||||
})
|
})
|
||||||
|
|
||||||
if child.Selectable() { child.Handle(tomo.EventSelect { }) }
|
|
||||||
window.resizeChildToFit()
|
window.resizeChildToFit()
|
||||||
}
|
}
|
||||||
window.childMinimumSizeChangeCallback(child.MinimumSize())
|
window.childMinimumSizeChangeCallback(child.MinimumSize())
|
||||||
@ -199,12 +202,18 @@ func (window *Window) redrawChildEntirely () {
|
|||||||
|
|
||||||
func (window *Window) resizeChildToFit () {
|
func (window *Window) resizeChildToFit () {
|
||||||
window.skipChildDrawCallback = true
|
window.skipChildDrawCallback = true
|
||||||
window.child.Handle(tomo.EventResize {
|
if child, ok := window.child.(tomo.Expanding); ok {
|
||||||
Width: window.metrics.width,
|
minimumHeight := child.MinimumHeightFor(window.metrics.width)
|
||||||
Height: window.metrics.height,
|
_, minimumWidth := child.MinimumSize()
|
||||||
})
|
window.childMinimumSizeChangeCallback (
|
||||||
|
minimumWidth, minimumHeight)
|
||||||
|
} else {
|
||||||
|
window.child.Resize (
|
||||||
|
window.metrics.width,
|
||||||
|
window.metrics.height)
|
||||||
|
window.redrawChildEntirely()
|
||||||
|
}
|
||||||
window.skipChildDrawCallback = false
|
window.skipChildDrawCallback = false
|
||||||
window.redrawChildEntirely()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) childDrawCallback (region tomo.Canvas) {
|
func (window *Window) childDrawCallback (region tomo.Canvas) {
|
||||||
@ -246,7 +255,9 @@ func (window *Window) childMinimumSizeChangeCallback (width, height int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) childSelectionRequestCallback () (granted bool) {
|
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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,8 +4,9 @@ import "image"
|
|||||||
import "image/draw"
|
import "image/draw"
|
||||||
import "image/color"
|
import "image/color"
|
||||||
|
|
||||||
// Canvas is like Image but also requires Set and SetRGBA methods. This
|
// Canvas is like draw.Image but is also able to return a raw pixel buffer for
|
||||||
// interface can be easily satisfied using an image.RGBA struct.
|
// more efficient drawing. This interface can be easily satisfied using a
|
||||||
|
// BasicCanvas struct.
|
||||||
type Canvas interface {
|
type Canvas interface {
|
||||||
draw.Image
|
draw.Image
|
||||||
Buffer () (data []color.RGBA, stride int)
|
Buffer () (data []color.RGBA, stride int)
|
||||||
|
175
element.go
Normal file
175
element.go
Normal 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)
|
||||||
|
}
|
@ -13,6 +13,7 @@ type Button struct {
|
|||||||
|
|
||||||
pressed bool
|
pressed bool
|
||||||
enabled bool
|
enabled bool
|
||||||
|
selected bool
|
||||||
onClick func ()
|
onClick func ()
|
||||||
|
|
||||||
text string
|
text string
|
||||||
@ -24,89 +25,103 @@ func NewButton (text string) (element *Button) {
|
|||||||
element = &Button { enabled: true }
|
element = &Button { enabled: true }
|
||||||
element.Core, element.core = core.NewCore(element)
|
element.Core, element.core = core.NewCore(element)
|
||||||
element.drawer.SetFace(theme.FontFaceRegular())
|
element.drawer.SetFace(theme.FontFaceRegular())
|
||||||
element.core.SetSelectable(true)
|
|
||||||
element.SetText(text)
|
element.SetText(text)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle handles an event.
|
func (element *Button) Resize (width, height int) {
|
||||||
func (element *Button) Handle (event tomo.Event) {
|
element.core.AllocateCanvas(width, height)
|
||||||
switch event.(type) {
|
element.draw()
|
||||||
case tomo.EventResize:
|
}
|
||||||
resizeEvent := event.(tomo.EventResize)
|
|
||||||
element.core.AllocateCanvas (
|
func (element *Button) HandleMouseDown (x, y int, button tomo.Button) {
|
||||||
resizeEvent.Width,
|
element.Select()
|
||||||
resizeEvent.Height)
|
if button != tomo.ButtonLeft { return }
|
||||||
|
element.pressed = true
|
||||||
|
if element.core.HasImage() {
|
||||||
element.draw()
|
element.draw()
|
||||||
|
element.core.PushAll()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case tomo.EventMouseDown:
|
func (element *Button) HandleMouseUp (x, y int, button tomo.Button) {
|
||||||
if !element.enabled { break }
|
if button != tomo.ButtonLeft { return }
|
||||||
|
element.pressed = false
|
||||||
|
if element.core.HasImage() {
|
||||||
|
element.draw()
|
||||||
|
element.core.PushAll()
|
||||||
|
}
|
||||||
|
|
||||||
mouseDownEvent := event.(tomo.EventMouseDown)
|
within := image.Point { x, y }.
|
||||||
element.Select()
|
In(element.Bounds())
|
||||||
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
|
element.pressed = true
|
||||||
if element.core.HasImage() {
|
if element.core.HasImage() {
|
||||||
element.draw()
|
element.draw()
|
||||||
element.core.PushAll()
|
element.core.PushAll()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
case tomo.EventKeyDown:
|
func (element *Button) HandleKeyUp(key tomo.Key, modifiers tomo.Modifiers) {
|
||||||
keyDownEvent := event.(tomo.EventKeyDown)
|
if key == tomo.KeyEnter && element.pressed {
|
||||||
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 }
|
|
||||||
element.pressed = false
|
element.pressed = false
|
||||||
if element.core.HasImage() {
|
if element.core.HasImage() {
|
||||||
element.draw()
|
element.draw()
|
||||||
element.core.PushAll()
|
element.core.PushAll()
|
||||||
}
|
}
|
||||||
|
if element.onClick != nil {
|
||||||
within := image.Point { mouseUpEvent.X, mouseUpEvent.Y }.
|
|
||||||
In(element.Bounds())
|
|
||||||
|
|
||||||
if within && element.onClick != nil {
|
|
||||||
element.onClick()
|
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.
|
// 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
|
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.
|
// SetEnabled sets whether this button can be clicked or not.
|
||||||
func (element *Button) SetEnabled (enabled bool) {
|
func (element *Button) SetEnabled (enabled bool) {
|
||||||
if element.enabled == enabled { return }
|
if element.enabled == enabled { return }
|
||||||
element.enabled = enabled
|
element.enabled = enabled
|
||||||
element.core.SetSelectable(enabled)
|
|
||||||
if element.core.HasImage () {
|
if element.core.HasImage () {
|
||||||
element.draw()
|
element.draw()
|
||||||
element.core.PushAll()
|
element.core.PushAll()
|
||||||
|
@ -12,10 +12,12 @@ type Container struct {
|
|||||||
*core.Core
|
*core.Core
|
||||||
core core.CoreControl
|
core core.CoreControl
|
||||||
|
|
||||||
layout tomo.Layout
|
layout tomo.Layout
|
||||||
children []tomo.LayoutEntry
|
children []tomo.LayoutEntry
|
||||||
drags [10]tomo.Element
|
drags [10]tomo.MouseTarget
|
||||||
warping bool
|
warping bool
|
||||||
|
selected bool
|
||||||
|
selectable bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContainer creates a new container.
|
// NewContainer creates a new container.
|
||||||
@ -44,18 +46,10 @@ func (element *Container) Adopt (child tomo.Element, expand bool) {
|
|||||||
MinimumSizeChange: func (int, int) {
|
MinimumSizeChange: func (int, int) {
|
||||||
element.updateMinimumSize()
|
element.updateMinimumSize()
|
||||||
},
|
},
|
||||||
SelectabilityChange: func (bool) {
|
|
||||||
element.updateSelectable()
|
|
||||||
},
|
|
||||||
SelectionRequest: func () (granted bool) {
|
SelectionRequest: func () (granted bool) {
|
||||||
if !child.Selectable() { return }
|
child, selectable := child.(tomo.Selectable)
|
||||||
if element.core.Select() {
|
if !selectable { return }
|
||||||
element.propogateToSelected(tomo.EventDeselect { })
|
return element.childSelectionRequestCallback(child)
|
||||||
child.Handle(tomo.EventSelect { })
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
},
|
},
|
||||||
Draw: func (region tomo.Canvas) {
|
Draw: func (region tomo.Canvas) {
|
||||||
element.drawChildRegion(child, region)
|
element.drawChildRegion(child, region)
|
||||||
@ -176,122 +170,168 @@ func (element *Container) childPosition (child tomo.Element) (position image.Poi
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Container) Handle (event tomo.Event) {
|
func (element *Container) Resize (width, height int) {
|
||||||
switch event.(type) {
|
element.core.AllocateCanvas(width, height)
|
||||||
case tomo.EventResize:
|
element.recalculate()
|
||||||
resizeEvent := event.(tomo.EventResize)
|
element.draw()
|
||||||
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) propogateToSelected (event tomo.Event) {
|
// TODO: implement KeyboardTarget
|
||||||
for _, entry := range element.children {
|
|
||||||
if entry.Selected() {
|
func (element *Container) HandleMouseDown (x, y int, button tomo.Button) {
|
||||||
entry.Handle(event)
|
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) {
|
func (element *Container) HandleScroll (x, y int, deltaX, deltaY float64) {
|
||||||
if !element.Selectable() { return }
|
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()
|
firstSelected := element.firstSelected()
|
||||||
if firstSelected < 0 {
|
if firstSelected < 0 {
|
||||||
for _, entry := range element.children {
|
found := false
|
||||||
if entry.Selectable() {
|
switch direction {
|
||||||
entry.Handle(tomo.EventSelect { })
|
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
|
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) {
|
func (element *Container) firstSelected () (index int) {
|
||||||
for currentIndex, entry := range element.children {
|
for currentIndex, entry := range element.children {
|
||||||
if entry.Selected() {
|
child, selectable := entry.Element.(tomo.Selectable)
|
||||||
|
if selectable && child.Selected() {
|
||||||
return currentIndex
|
return currentIndex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -299,15 +339,36 @@ func (element *Container) firstSelected () (index int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (element *Container) updateSelectable () {
|
func (element *Container) updateSelectable () {
|
||||||
selectable := false
|
element.selectable = false
|
||||||
for _, entry := range element.children {
|
element.forSelectable (func (tomo.Selectable) bool {
|
||||||
if entry.Selectable() { selectable = true }
|
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 () {
|
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 () {
|
func (element *Container) recalculate () {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
package basic
|
package basic
|
||||||
|
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
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/elements/core"
|
||||||
@ -28,20 +27,13 @@ func NewLabel (text string, wrap bool) (element *Label) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle handles and event.
|
func (element *Label) Resize (width, height int) {
|
||||||
func (element *Label) Handle (event tomo.Event) {
|
element.core.AllocateCanvas(width, height)
|
||||||
switch event.(type) {
|
if element.wrap {
|
||||||
case tomo.EventResize:
|
element.drawer.SetMaxWidth(width)
|
||||||
resizeEvent := event.(tomo.EventResize)
|
element.drawer.SetMaxHeight(height)
|
||||||
element.core.AllocateCanvas (
|
|
||||||
resizeEvent.Width,
|
|
||||||
resizeEvent.Height)
|
|
||||||
if element.wrap {
|
|
||||||
element.drawer.SetMaxWidth (resizeEvent.Width)
|
|
||||||
element.drawer.SetMaxHeight(resizeEvent.Height)
|
|
||||||
}
|
|
||||||
element.draw()
|
|
||||||
}
|
}
|
||||||
|
element.draw()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,89 +27,87 @@ func NewCore (parent tomo.Element) (core *Core, control CoreControl) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (core Core) ColorModel () (model color.Model) {
|
// ColorModel fulfills the draw.Image interface.
|
||||||
|
func (core *Core) ColorModel () (model color.Model) {
|
||||||
return color.RGBAModel
|
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)
|
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()
|
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)
|
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()
|
return core.canvas.Buffer()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (core Core) Selectable () (selectable bool) {
|
// MinimumSize fulfils the tomo.Element interface. This should not need to be
|
||||||
return core.selectable
|
// overridden.
|
||||||
}
|
func (core *Core) MinimumSize () (width, height int) {
|
||||||
|
return core.metrics.minimumWidth, core.metrics.minimumHeight
|
||||||
func (core Core) Selected () (selected bool) {
|
|
||||||
return core.selected
|
|
||||||
}
|
|
||||||
|
|
||||||
func (core Core) AdvanceSelection (direction int) (ok bool) {
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetParentHooks fulfils the tomo.Element interface. This should not need to be
|
||||||
|
// overridden.
|
||||||
func (core *Core) SetParentHooks (hooks tomo.ParentHooks) {
|
func (core *Core) SetParentHooks (hooks tomo.ParentHooks) {
|
||||||
core.hooks = hooks
|
core.hooks = hooks
|
||||||
}
|
}
|
||||||
|
|
||||||
func (core Core) MinimumSize () (width, height int) {
|
// CoreControl is a struct that can exert control over a Core struct. It can be
|
||||||
return core.metrics.minimumWidth, core.metrics.minimumHeight
|
// 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.
|
||||||
// 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.
|
|
||||||
type CoreControl struct {
|
type CoreControl struct {
|
||||||
tomo.BasicCanvas
|
tomo.BasicCanvas
|
||||||
core *Core
|
core *Core
|
||||||
}
|
}
|
||||||
|
|
||||||
func (control CoreControl) HasImage () (empty bool) {
|
// RequestSelection requests that the element's parent send it a selection
|
||||||
return !control.Bounds().Empty()
|
// event. If the request was granted, it returns true. If it was denied, it
|
||||||
}
|
// returns false.
|
||||||
|
func (control CoreControl) RequestSelection () (granted bool) {
|
||||||
func (control CoreControl) Select () (granted bool) {
|
|
||||||
return control.core.hooks.RunSelectionRequest()
|
return control.core.hooks.RunSelectionRequest()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (control CoreControl) SetSelected (selected bool) {
|
// HasImage returns true if the core has an allocated image buffer, and false if
|
||||||
if !control.core.selectable { return }
|
// it doesn't.
|
||||||
control.core.selected = selected
|
func (control CoreControl) HasImage () (has bool) {
|
||||||
}
|
return !control.Bounds().Empty()
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func (control CoreControl) PushRegion (bounds image.Rectangle) {
|
||||||
control.core.hooks.RunDraw(tomo.Cut(control, bounds))
|
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 () {
|
func (control CoreControl) PushAll () {
|
||||||
control.PushRegion(control.Bounds())
|
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) {
|
func (control *CoreControl) AllocateCanvas (width, height int) {
|
||||||
core := control.core
|
|
||||||
width, height, _ = control.ConstrainSize(width, height)
|
width, height, _ = control.ConstrainSize(width, height)
|
||||||
core.canvas = tomo.NewBasicCanvas(width, height)
|
control.core.canvas = tomo.NewBasicCanvas(width, height)
|
||||||
control.BasicCanvas = core.canvas
|
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) {
|
func (control CoreControl) SetMinimumSize (width, height int) {
|
||||||
core := control.core
|
core := control.core
|
||||||
if width == core.metrics.minimumWidth &&
|
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
|
// if there is an image buffer, and the current size is less
|
||||||
// than this new minimum size, send core.parent a resize event.
|
// than this new minimum size, send core.parent a resize event.
|
||||||
bounds := control.Bounds()
|
if control.HasImage() {
|
||||||
imageWidth,
|
bounds := control.Bounds()
|
||||||
imageHeight,
|
imageWidth,
|
||||||
constrained := control.ConstrainSize (
|
imageHeight,
|
||||||
bounds.Dx(),
|
constrained := control.ConstrainSize(bounds.Dx(), bounds.Dy())
|
||||||
bounds.Dy())
|
if constrained {
|
||||||
if constrained {
|
core.parent.Resize(imageWidth, imageHeight)
|
||||||
core.parent.Handle (tomo.EventResize {
|
}
|
||||||
Width: imageWidth,
|
|
||||||
Height: 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 (
|
func (control CoreControl) ConstrainSize (
|
||||||
inWidth, inHeight int,
|
inWidth, inHeight int,
|
||||||
) (
|
) (
|
||||||
|
@ -3,7 +3,6 @@ package fun
|
|||||||
import "time"
|
import "time"
|
||||||
import "math"
|
import "math"
|
||||||
import "image"
|
import "image"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
||||||
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/elements/core"
|
||||||
@ -21,15 +20,9 @@ func NewAnalogClock (newTime time.Time) (element *AnalogClock) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *AnalogClock) Handle (event tomo.Event) {
|
func (element *AnalogClock) Resize (width, height int) {
|
||||||
switch event.(type) {
|
element.core.AllocateCanvas(width, height)
|
||||||
case tomo.EventResize:
|
element.draw()
|
||||||
resizeEvent := event.(tomo.EventResize)
|
|
||||||
element.core.AllocateCanvas (
|
|
||||||
resizeEvent.Width,
|
|
||||||
resizeEvent.Height)
|
|
||||||
element.draw()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *AnalogClock) SetTime (newTime time.Time) {
|
func (element *AnalogClock) SetTime (newTime time.Time) {
|
||||||
|
@ -26,58 +26,47 @@ func NewMouse () (element *Mouse) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (element *Mouse) Handle (event tomo.Event) {
|
func (element *Mouse) Resize (width, height int) {
|
||||||
switch event.(type) {
|
element.core.AllocateCanvas(width, height)
|
||||||
case tomo.EventResize:
|
artist.FillRectangle (
|
||||||
resizeEvent := event.(tomo.EventResize)
|
element.core,
|
||||||
element.core.AllocateCanvas (
|
theme.AccentPattern(),
|
||||||
resizeEvent.Width,
|
element.Bounds())
|
||||||
resizeEvent.Height)
|
artist.StrokeRectangle (
|
||||||
artist.FillRectangle (
|
element.core,
|
||||||
element.core,
|
artist.NewUniform(color.Black), 1,
|
||||||
theme.AccentPattern(),
|
element.Bounds())
|
||||||
element.Bounds())
|
artist.Line (
|
||||||
artist.StrokeRectangle (
|
element.core, artist.NewUniform(color.White), 3,
|
||||||
element.core,
|
image.Pt(1, 1),
|
||||||
artist.NewUniform(color.Black), 1,
|
image.Pt(width - 2, height - 2))
|
||||||
element.Bounds())
|
artist.Line (
|
||||||
artist.Line (
|
element.core, artist.NewUniform(color.White), 1,
|
||||||
element.core, artist.NewUniform(color.White), 3,
|
image.Pt(1, height - 2),
|
||||||
image.Pt(1, 1),
|
image.Pt(width - 2, 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) 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) { }
|
||||||
|
88
event.go
88
event.go
@ -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 { }
|
|
@ -1,8 +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/basic"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
|
@ -2,8 +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/basic"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
|
@ -3,9 +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/fun"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/fun"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
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/basic"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
|
@ -2,8 +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/basic"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
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/basic"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/basic"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
import "git.tebibyte.media/sashakoshka/tomo/elements/testing"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/elements/layouts"
|
|
||||||
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
import _ "git.tebibyte.media/sashakoshka/tomo/backends/x"
|
||||||
|
|
||||||
func main () {
|
func main () {
|
||||||
|
27
layout.go
Normal file
27
layout.go
Normal 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)
|
||||||
|
}
|
@ -4,6 +4,11 @@ import "image"
|
|||||||
import "git.tebibyte.media/sashakoshka/tomo"
|
import "git.tebibyte.media/sashakoshka/tomo"
|
||||||
import "git.tebibyte.media/sashakoshka/tomo/theme"
|
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 {
|
type Dialog struct {
|
||||||
// If Gap is true, a gap will be placed between each element.
|
// If Gap is true, a gap will be placed between each element.
|
||||||
Gap bool
|
Gap bool
|
||||||
@ -39,10 +44,7 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, width, height int) {
|
|||||||
mainBounds := entries[0].Bounds()
|
mainBounds := entries[0].Bounds()
|
||||||
if mainBounds.Dy() != mainHeight ||
|
if mainBounds.Dy() != mainHeight ||
|
||||||
mainBounds.Dx() != width {
|
mainBounds.Dx() != width {
|
||||||
entries[0].Handle (tomo.EventResize {
|
entries[0].Resize(width, mainHeight)
|
||||||
Width: width,
|
|
||||||
Height: mainHeight,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -94,10 +96,7 @@ func (layout Dialog) Arrange (entries []tomo.LayoutEntry, width, height int) {
|
|||||||
entryBounds := entry.Bounds()
|
entryBounds := entry.Bounds()
|
||||||
if entryBounds.Dy() != controlRowHeight ||
|
if entryBounds.Dy() != controlRowHeight ||
|
||||||
entryBounds.Dx() != entryWidth {
|
entryBounds.Dx() != entryWidth {
|
||||||
entry.Handle (tomo.EventResize {
|
entry.Resize(entryWidth, controlRowHeight)
|
||||||
Width: entryWidth,
|
|
||||||
Height: 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
|
// MinimumSize returns the minimum width and height that will be needed to
|
||||||
// arrange the given list of entries.
|
// 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 {
|
if len(entries) > 0 {
|
||||||
mainChildHeight := 0
|
mainChildHeight := 0
|
||||||
width, mainChildHeight = entries[0].MinimumSize()
|
width, mainChildHeight = entries[0].MinimumSize()
|
@ -63,17 +63,19 @@ func (layout Horizontal) Arrange (entries []tomo.LayoutEntry, width, height int)
|
|||||||
x += entryWidth
|
x += entryWidth
|
||||||
entryBounds := entry.Bounds()
|
entryBounds := entry.Bounds()
|
||||||
if entryBounds.Dy() != height || entryBounds.Dx() != entryWidth {
|
if entryBounds.Dy() != height || entryBounds.Dx() != entryWidth {
|
||||||
entry.Handle (tomo.EventResize {
|
entry.Resize(entryWidth, height)
|
||||||
Width: entryWidth,
|
|
||||||
Height: height,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MinimumSize returns the minimum width and height that will be needed to
|
// MinimumSize returns the minimum width and height that will be needed to
|
||||||
// arrange the given list of entries.
|
// 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 {
|
for index, entry := range entries {
|
||||||
entryWidth, entryHeight := entry.MinimumSize()
|
entryWidth, entryHeight := entry.MinimumSize()
|
||||||
if entryHeight > height {
|
if entryHeight > height {
|
@ -63,18 +63,19 @@ func (layout Vertical) Arrange (entries []tomo.LayoutEntry, width, height int) {
|
|||||||
y += entryHeight
|
y += entryHeight
|
||||||
entryBounds := entry.Bounds()
|
entryBounds := entry.Bounds()
|
||||||
if entryBounds.Dx() != width || entryBounds.Dy() != entryHeight {
|
if entryBounds.Dx() != width || entryBounds.Dy() != entryHeight {
|
||||||
// println(entryHeight)
|
entry.Resize(width, entryHeight)
|
||||||
entry.Handle (tomo.EventResize {
|
|
||||||
Width: width,
|
|
||||||
Height: entryHeight,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MinimumSize returns the minimum width and height that will be needed to
|
// MinimumSize returns the minimum width and height that will be needed to
|
||||||
// arrange the given list of entries.
|
// 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 {
|
for index, entry := range entries {
|
||||||
entryWidth, entryHeight := entry.MinimumSize()
|
entryWidth, entryHeight := entry.MinimumSize()
|
||||||
if entryWidth > width {
|
if entryWidth > width {
|
@ -1,8 +1,8 @@
|
|||||||
package popups
|
package popups
|
||||||
|
|
||||||
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/basic"
|
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.
|
// DialogKind defines the semantic role of a dialog window.
|
||||||
type DialogKind int
|
type DialogKind int
|
||||||
|
158
tomo.go
158
tomo.go
@ -1,165 +1,7 @@
|
|||||||
package tomo
|
package tomo
|
||||||
|
|
||||||
import "image"
|
|
||||||
import "errors"
|
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
|
var backend Backend
|
||||||
|
|
||||||
// Run initializes a backend, calls the callback function, and begins the event
|
// Run initializes a backend, calls the callback function, and begins the event
|
||||||
|
39
window.go
Normal file
39
window.go
Normal 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 ())
|
||||||
|
}
|
Reference in New Issue
Block a user