diff --git a/artist/text.go b/artist/text.go index 59bd4e6..2953c03 100644 --- a/artist/text.go +++ b/artist/text.go @@ -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. diff --git a/backends/x/event.go b/backends/x/event.go index 97344a8..8e8c1d5 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -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)) + } } diff --git a/backends/x/window.go b/backends/x/window.go index b2f3f7b..aeffb03 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -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 } diff --git a/canvas.go b/canvas.go index f51c74a..51cd06b 100644 --- a/canvas.go +++ b/canvas.go @@ -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) diff --git a/element.go b/element.go new file mode 100644 index 0000000..61a4b1f --- /dev/null +++ b/element.go @@ -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) +} diff --git a/elements/basic/button.go b/elements/basic/button.go index d7e80e7..bb03733 100644 --- a/elements/basic/button.go +++ b/elements/basic/button.go @@ -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() diff --git a/elements/basic/container.go b/elements/basic/container.go index d3eb74b..e8d58c9 100644 --- a/elements/basic/container.go +++ b/elements/basic/container.go @@ -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 () { diff --git a/elements/basic/label.go b/elements/basic/label.go index d80f781..1b33373 100644 --- a/elements/basic/label.go +++ b/elements/basic/label.go @@ -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 } diff --git a/elements/core/core.go b/elements/core/core.go index cb12959..9f0fd3e 100644 --- a/elements/core/core.go +++ b/elements/core/core.go @@ -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, ) ( diff --git a/elements/fun/clock.go b/elements/fun/clock.go index 4e532bd..b168e55 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -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) { diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index 2c3bfa6..45653aa 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -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) { } diff --git a/event.go b/event.go deleted file mode 100644 index dd2400a..0000000 --- a/event.go +++ /dev/null @@ -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 { } diff --git a/examples/dialogLayout/main.go b/examples/dialogLayout/main.go index a19f102..d2bc039 100644 --- a/examples/dialogLayout/main.go +++ b/examples/dialogLayout/main.go @@ -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 () { diff --git a/examples/flow/main.go b/examples/flow/main.go index 75aec57..5d9d7ef 100644 --- a/examples/flow/main.go +++ b/examples/flow/main.go @@ -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 () { diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index e41504c..9fc02c6 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -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 () { diff --git a/examples/horizontalLayout/main.go b/examples/horizontalLayout/main.go index c1fed69..28f1699 100644 --- a/examples/horizontalLayout/main.go +++ b/examples/horizontalLayout/main.go @@ -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 () { diff --git a/examples/popups/main.go b/examples/popups/main.go index fdf726e..1513d40 100644 --- a/examples/popups/main.go +++ b/examples/popups/main.go @@ -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 () { diff --git a/examples/verticalLayout/main.go b/examples/verticalLayout/main.go index bf01e80..c96b86c 100644 --- a/examples/verticalLayout/main.go +++ b/examples/verticalLayout/main.go @@ -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 () { diff --git a/layout.go b/layout.go new file mode 100644 index 0000000..7a3e458 --- /dev/null +++ b/layout.go @@ -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) +} diff --git a/elements/layouts/dialog.go b/layouts/dialog.go similarity index 85% rename from elements/layouts/dialog.go rename to layouts/dialog.go index e166cb1..f7915b9 100644 --- a/elements/layouts/dialog.go +++ b/layouts/dialog.go @@ -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() diff --git a/elements/layouts/horizontal.go b/layouts/horizontal.go similarity index 92% rename from elements/layouts/horizontal.go rename to layouts/horizontal.go index 0f08a24..5ca5000 100644 --- a/elements/layouts/horizontal.go +++ b/layouts/horizontal.go @@ -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 { diff --git a/elements/layouts/vertical.go b/layouts/vertical.go similarity index 91% rename from elements/layouts/vertical.go rename to layouts/vertical.go index 971729c..c366419 100644 --- a/elements/layouts/vertical.go +++ b/layouts/vertical.go @@ -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 { diff --git a/popups/dialog.go b/popups/dialog.go index f0a3e0e..d8635c9 100644 --- a/popups/dialog.go +++ b/popups/dialog.go @@ -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 diff --git a/tomo.go b/tomo.go index 134eccc..b6d9ab0 100644 --- a/tomo.go +++ b/tomo.go @@ -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 diff --git a/window.go b/window.go new file mode 100644 index 0000000..94980f8 --- /dev/null +++ b/window.go @@ -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 ()) +}