diff --git a/backend.go b/backend.go index b6d9e83..238a877 100644 --- a/backend.go +++ b/backend.go @@ -17,6 +17,9 @@ type Backend interface { // possible. This method must be safe to call from other threads. Do (callback func ()) + // NewEntity creates a new entity for the specified element. + NewEntity (owner Element) Entity + // NewWindow creates a new window within the specified bounding // rectangle. The position on screen may be overridden by the backend or // operating system. diff --git a/backends/x/entity.go b/backends/x/entity.go new file mode 100644 index 0000000..bcd5ee1 --- /dev/null +++ b/backends/x/entity.go @@ -0,0 +1,278 @@ +package x + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +type entity struct { + window *window + parent *entity + children []*entity + element tomo.Element + + bounds image.Rectangle + clippedBounds image.Rectangle + minWidth int + minHeight int + + selected bool + layoutInvalid bool + isContainer bool +} + +func (backend *Backend) NewEntity (owner tomo.Element) tomo.Entity { + entity := &entity { element: owner } + if _, ok := owner.(tomo.Container); ok { + entity.isContainer = true + entity.InvalidateLayout() + } + return entity +} + +func (ent *entity) unlink () { + ent.propagate (func (child *entity) bool { + if child.window != nil { + delete(ent.window.system.drawingInvalid, child) + } + child.window = nil + return true + }) + + if ent.window != nil { + delete(ent.window.system.drawingInvalid, ent) + } + ent.parent = nil + ent.window = nil + + if element, ok := ent.element.(tomo.Selectable); ok { + ent.selected = false + element.HandleSelectionChange() + } +} + +func (entity *entity) link (parent *entity) { + entity.parent = parent + entity.clip(parent.clippedBounds) + if parent.window != nil { + entity.setWindow(parent.window) + } +} + +func (ent *entity) setWindow (window *window) { + ent.window = window + ent.Invalidate() + ent.InvalidateLayout() + ent.propagate (func (child *entity) bool { + child.window = window + ent.Invalidate() + ent.InvalidateLayout() + return true + }) +} + +func (entity *entity) propagate (callback func (*entity) bool) bool { + for _, child := range entity.children { + if !child.propagate(callback) { + return false + } + } + return callback(entity) +} + +func (entity *entity) childAt (point image.Point) *entity { + for _, child := range entity.children { + if point.In(child.bounds) { + return child.childAt(point) + } + } + return entity +} + +func (entity *entity) scrollTargetChildAt (point image.Point) *entity { + for _, child := range entity.children { + if point.In(child.bounds) { + result := child.scrollTargetChildAt(point) + if result != nil { return result } + break + } + } + + if _, ok := entity.element.(tomo.ScrollTarget); ok { + return entity + } + return nil +} + +func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer, tomo.Element)) { + if entity.parent == nil { return } + if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok { + callback(parent, entity.element) + } + entity.parent.forMouseTargetContainers(callback) +} + +func (entity *entity) clip (bounds image.Rectangle) { + entity.clippedBounds = entity.bounds.Intersect(bounds) + for _, child := range entity.children { + child.clip(entity.clippedBounds) + } +} + +// ----------- Entity ----------- // + +func (entity *entity) Invalidate () { + if entity.window == nil { return } + if entity.window.system.invalidateIgnore { return } + entity.window.drawingInvalid.Add(entity) +} + +func (entity *entity) Bounds () image.Rectangle { + return entity.bounds +} + +func (entity *entity) Window () tomo.Window { + return entity.window +} + +func (entity *entity) SetMinimumSize (width, height int) { + entity.minWidth = width + entity.minHeight = height + if entity.parent == nil { + if entity.window != nil { + entity.window.setMinimumSize(width, height) + } + } else { + entity.parent.element.(tomo.Container). + HandleChildMinimumSizeChange(entity.element) + } +} + +func (entity *entity) DrawBackground (destination canvas.Canvas) { + if entity.parent != nil { + entity.parent.element.(tomo.Container).DrawBackground(destination) + } else if entity.window != nil { + entity.window.system.theme.Pattern ( + tomo.PatternBackground, + tomo.State { }).Draw ( + destination, + entity.window.canvas.Bounds()) + } +} + +// ----------- ContainerEntity ----------- // + +func (entity *entity) InvalidateLayout () { + if entity.window == nil { return } + if !entity.isContainer { return } + entity.layoutInvalid = true + entity.window.system.anyLayoutInvalid = true +} + +func (ent *entity) Adopt (child tomo.Element) { + childEntity, ok := child.Entity().(*entity) + if !ok || childEntity == nil { return } + childEntity.link(ent) + ent.children = append(ent.children, childEntity) +} + +func (ent *entity) Insert (index int, child tomo.Element) { + childEntity, ok := child.Entity().(*entity) + if !ok || childEntity == nil { return } + ent.children = append ( + ent.children[:index + 1], + ent.children[index:]...) + ent.children[index] = childEntity +} + +func (entity *entity) Disown (index int) { + entity.children[index].unlink() + entity.children = append ( + entity.children[:index], + entity.children[index + 1:]...) +} + +func (entity *entity) IndexOf (child tomo.Element) int { + for index, childEntity := range entity.children { + if childEntity.element == child { + return index + } + } + + return -1 +} + +func (entity *entity) Child (index int) tomo.Element { + return entity.children[index].element +} + +func (entity *entity) CountChildren () int { + return len(entity.children) +} + +func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { + child := entity.children[index] + child.bounds = bounds + child.clip(entity.clippedBounds) + child.Invalidate() + child.InvalidateLayout() +} + +func (entity *entity) SelectChild (index int, selected bool) { + child := entity.children[index] + if element, ok := child.element.(tomo.Selectable); ok { + if child.selected == selected { return } + child.selected = selected + element.HandleSelectionChange() + } +} + +func (entity *entity) ChildMinimumSize (index int) (width, height int) { + childEntity := entity.children[index] + return childEntity.minWidth, childEntity.minHeight +} + +// ----------- FocusableEntity ----------- // + +func (entity *entity) Focused () bool { + if entity.window == nil { return false } + return entity.window.focused == entity +} + +func (entity *entity) Focus () { + if entity.window == nil { return } + entity.window.system.focus(entity) +} + +func (entity *entity) FocusNext () { + entity.window.system.focusNext() +} + +func (entity *entity) FocusPrevious () { + entity.window.system.focusPrevious() +} + +// ----------- SelectableEntity ----------- // + +func (entity *entity) Selected () bool { + return entity.selected +} + +// ----------- FlexibleEntity ----------- // + +func (entity *entity) NotifyFlexibleHeightChange () { + if entity.parent == nil { return } + if parent, ok := entity.parent.element.(tomo.FlexibleContainer); ok { + parent.HandleChildFlexibleHeightChange ( + entity.element.(tomo.Flexible)) + } +} + +// ----------- ScrollableEntity ----------- // + +func (entity *entity) NotifyScrollBoundsChange () { + if entity.parent == nil { return } + if parent, ok := entity.parent.element.(tomo.ScrollableContainer); ok { + parent.HandleChildScrollBoundsChange ( + entity.element.(tomo.Scrollable)) + } +} diff --git a/backends/x/event.go b/backends/x/event.go index 4675c2f..c099268 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -20,28 +20,19 @@ func (sum *scrollSum) add (button xproto.Button, window *window, state uint16) { (state & window.backend.modifierMasks.shiftLock) > 0 if shift { switch button { - case 4: - sum.x -= scrollDistance - case 5: - sum.x += scrollDistance - case 6: - sum.y -= scrollDistance - case 7: - sum.y += scrollDistance + case 4: sum.x -= scrollDistance + case 5: sum.x += scrollDistance + case 6: sum.y -= scrollDistance + case 7: sum.y += scrollDistance } } else { switch button { - case 4: - sum.y -= scrollDistance - case 5: - sum.y += scrollDistance - case 6: - sum.x -= scrollDistance - case 7: - sum.x += scrollDistance + case 4: sum.y -= scrollDistance + case 5: sum.y += scrollDistance + case 6: sum.x -= scrollDistance + case 7: sum.x += scrollDistance } } - } func (window *window) handleExpose ( @@ -49,6 +40,7 @@ func (window *window) handleExpose ( event xevent.ExposeEvent, ) { _, region := window.compressExpose(*event.ExposeEvent) + window.system.afterEvent() window.pushRegion(region) } @@ -74,7 +66,6 @@ func (window *window) handleConfigureNotify ( window.updateBounds ( configureEvent.X, configureEvent.Y, configureEvent.Width, configureEvent.Height) - if sizeChanged { configureEvent = window.compressConfigureNotify(configureEvent) @@ -85,8 +76,11 @@ func (window *window) handleConfigureNotify ( window.resizeChildToFit() if !window.exposeEventFollows(configureEvent) { - window.redrawChildEntirely() + window.child.Invalidate() + window.child.InvalidateLayout() } + + window.system.afterEvent() } } @@ -127,8 +121,7 @@ func (window *window) handleKeyPress ( connection *xgbutil.XUtil, event xevent.KeyPressEvent, ) { - if window.child == nil { return } - if window.hasModal { return } + if window.hasModal { return } keyEvent := *event.KeyPressEvent key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State) @@ -136,29 +129,25 @@ func (window *window) handleKeyPress ( modifiers.NumberPad = numberPad if key == input.KeyTab && modifiers.Alt { - if child, ok := window.child.(tomo.Focusable); ok { - direction := input.KeynavDirectionForward - if modifiers.Shift { - direction = input.KeynavDirectionBackward - } - - if !child.HandleFocus(direction) { - child.HandleUnfocus() - } + if modifiers.Shift { + window.system.focusPrevious() + } else { + window.system.focusNext() } } else if key == input.KeyEscape && window.shy { window.Close() - } else if child, ok := window.child.(tomo.KeyboardTarget); ok { - child.HandleKeyDown(key, modifiers) + } else if window.focused != nil { + focused, ok := window.focused.element.(tomo.KeyboardTarget) + if ok { focused.HandleKeyDown(key, modifiers) } } + + window.system.afterEvent() } func (window *window) handleKeyRelease ( connection *xgbutil.XUtil, event xevent.KeyReleaseEvent, ) { - if window.child == nil { return } - keyEvent := *event.KeyReleaseEvent // do not process this event if it was generated from a key repeat @@ -181,9 +170,12 @@ func (window *window) handleKeyRelease ( key, numberPad := window.backend.keycodeToKey(keyEvent.Detail, keyEvent.State) modifiers := window.modifiersFromState(keyEvent.State) modifiers.NumberPad = numberPad - - if child, ok := window.child.(tomo.KeyboardTarget); ok { - child.HandleKeyUp(key, modifiers) + + if window.focused != nil { + focused, ok := window.focused.element.(tomo.KeyboardTarget) + if ok { focused.HandleKeyUp(key, modifiers) } + + window.system.afterEvent() } } @@ -191,68 +183,99 @@ func (window *window) handleButtonPress ( connection *xgbutil.XUtil, event xevent.ButtonPressEvent, ) { - if window.child == nil { return } - if window.hasModal { return } + if window.hasModal { return } - buttonEvent := *event.ButtonPressEvent - - insideWindow := image.Pt ( - int(buttonEvent.EventX), - int(buttonEvent.EventY)).In(window.canvas.Bounds()) + buttonEvent := *event.ButtonPressEvent + point := image.Pt(int(buttonEvent.EventX), int(buttonEvent.EventY)) + insideWindow := point.In(window.canvas.Bounds()) + scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 - scrolling := buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 - if !insideWindow && window.shy && !scrolling { window.Close() } else if scrolling { - if child, ok := window.child.(tomo.ScrollTarget); ok { - sum := scrollSum { } - sum.add(buttonEvent.Detail, window, buttonEvent.State) - window.compressScrollSum(buttonEvent, &sum) - child.HandleScroll ( - int(buttonEvent.EventX), - int(buttonEvent.EventY), - float64(sum.x), float64(sum.y)) + underneath := window.system.scrollTargetChildAt(point) + if underneath != nil { + if child, ok := underneath.element.(tomo.ScrollTarget); ok { + sum := scrollSum { } + sum.add(buttonEvent.Detail, window, buttonEvent.State) + window.compressScrollSum(buttonEvent, &sum) + child.HandleScroll ( + point.X, point.Y, + float64(sum.x), float64(sum.y)) + } } } else { - if child, ok := window.child.(tomo.MouseTarget); ok { + underneath := window.system.childAt(point) + window.system.drags[buttonEvent.Detail] = underneath + if child, ok := underneath.element.(tomo.MouseTarget); ok { child.HandleMouseDown ( - int(buttonEvent.EventX), - int(buttonEvent.EventY), + point.X, point.Y, input.Button(buttonEvent.Detail)) } + callback := func (container tomo.MouseTargetContainer, child tomo.Element) { + container.HandleChildMouseDown ( + point.X, point.Y, + input.Button(buttonEvent.Detail), + child) + } + underneath.forMouseTargetContainers(callback) } + window.system.afterEvent() } func (window *window) handleButtonRelease ( connection *xgbutil.XUtil, event xevent.ButtonReleaseEvent, ) { - if window.child == nil { return } - - if child, ok := window.child.(tomo.MouseTarget); ok { - buttonEvent := *event.ButtonReleaseEvent - if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } - child.HandleMouseUp ( + buttonEvent := *event.ButtonReleaseEvent + if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } + dragging := window.system.drags[buttonEvent.Detail] + if dragging != nil { + if child, ok := dragging.element.(tomo.MouseTarget); ok { + child.HandleMouseUp ( + int(buttonEvent.EventX), + int(buttonEvent.EventY), + input.Button(buttonEvent.Detail)) + } + callback := func (container tomo.MouseTargetContainer, child tomo.Element) { + container.HandleChildMouseUp ( int(buttonEvent.EventX), int(buttonEvent.EventY), - input.Button(buttonEvent.Detail)) + input.Button(buttonEvent.Detail), + child) + } + dragging.forMouseTargetContainers(callback) } + + window.system.afterEvent() } func (window *window) handleMotionNotify ( connection *xgbutil.XUtil, event xevent.MotionNotifyEvent, ) { - if window.child == nil { return } - - if child, ok := window.child.(tomo.MotionTarget); ok { - motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) - child.HandleMotion ( - int(motionEvent.EventX), - int(motionEvent.EventY)) + motionEvent := window.compressMotionNotify(*event.MotionNotifyEvent) + x := int(motionEvent.EventX) + y :=int(motionEvent.EventY) + + handled := false + for _, child := range window.system.drags { + if child == nil { continue } + if child, ok := child.element.(tomo.MotionTarget); ok { + child.HandleMotion(x, y) + handled = true + } } + + if !handled { + child := window.system.childAt(image.Pt(x, y)) + if child, ok := child.element.(tomo.MotionTarget); ok { + child.HandleMotion(x, y) + } + } + + window.system.afterEvent() } func (window *window) handleSelectionNotify ( diff --git a/backends/x/selection.go b/backends/x/selection.go index 886dc2c..befe208 100644 --- a/backends/x/selection.go +++ b/backends/x/selection.go @@ -97,11 +97,13 @@ func (request *selectionRequest) convertSelection ( func (request *selectionRequest) die (err error) { request.callback(nil, err) + request.window.system.afterEvent() request.state = selReqStateClosed } func (request *selectionRequest) finalize (data data.Data) { request.callback(data, nil) + request.window.system.afterEvent() request.state = selReqStateClosed } diff --git a/backends/x/system.go b/backends/x/system.go new file mode 100644 index 0000000..ac2b5a7 --- /dev/null +++ b/backends/x/system.go @@ -0,0 +1,186 @@ +package x + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" +import "git.tebibyte.media/sashakoshka/tomo/default/config" + +type entitySet map[*entity] struct { } + +func (set entitySet) Empty () bool { + return len(set) == 0 +} + +func (set entitySet) Has (entity *entity) bool { + _, ok := set[entity] + return ok +} + +func (set entitySet) Add (entity *entity) { + set[entity] = struct { } { } +} + +type system struct { + child *entity + focused *entity + canvas canvas.BasicCanvas + + theme theme.Wrapped + config config.Wrapped + + invalidateIgnore bool + drawingInvalid entitySet + anyLayoutInvalid bool + + drags [10]*entity + + pushFunc func (image.Rectangle) +} + +func (system *system) initialize () { + system.drawingInvalid = make(entitySet) +} + +func (system *system) SetTheme (theme tomo.Theme) { + system.theme.Theme = theme + system.propagate (func (entity *entity) bool { + if child, ok := system.child.element.(tomo.Themeable); ok { + child.SetTheme(theme) + } + return true + }) +} + +func (system *system) SetConfig (config tomo.Config) { + system.config.Config = config + system.propagate (func (entity *entity) bool { + if child, ok := system.child.element.(tomo.Configurable); ok { + child.SetConfig(config) + } + return true + }) +} + +func (system *system) focus (entity *entity) { + previous := system.focused + system.focused = entity + if previous != nil { + previous.element.(tomo.Focusable).HandleFocusChange() + } + if entity != nil { + entity.element.(tomo.Focusable).HandleFocusChange() + } +} + +func (system *system) focusNext () { + found := system.focused == nil + focused := false + system.propagate (func (entity *entity) bool { + if found { + // looking for the next element to select + child, ok := entity.element.(tomo.Focusable) + if ok && child.Enabled() { + // found it + entity.Focus() + focused = true + return false + } + } else { + // looking for the current focused element + if entity == system.focused { + // found it + found = true + } + } + return true + }) + + if !focused { system.focus(nil) } +} + +func (system *system) focusPrevious () { + var behind *entity + system.propagate (func (entity *entity) bool { + if entity == system.focused { + return false + } + + child, ok := entity.element.(tomo.Focusable) + if ok && child.Enabled() { behind = entity } + return true + }) + system.focus(behind) +} + +func (system *system) propagate (callback func (*entity) bool) { + if system.child == nil { return } + system.child.propagate(callback) +} + +func (system *system) childAt (point image.Point) *entity { + if system.child == nil { return nil } + return system.child.childAt(point) +} + +func (system *system) scrollTargetChildAt (point image.Point) *entity { + if system.child == nil { return nil } + return system.child.scrollTargetChildAt(point) +} + +func (system *system) resizeChildToFit () { + system.child.bounds = system.canvas.Bounds() + system.child.clippedBounds = system.child.bounds + system.child.Invalidate() + if system.child.isContainer { + system.child.InvalidateLayout() + } +} + +func (system *system) afterEvent () { + if system.anyLayoutInvalid { + system.layout(system.child, false) + system.anyLayoutInvalid = false + } + system.draw() +} + +func (system *system) layout (entity *entity, force bool) { + if entity == nil { return } + if entity.layoutInvalid == true || force { + if element, ok := entity.element.(tomo.Layoutable); ok { + element.Layout() + entity.layoutInvalid = false + force = true + } + } + + for _, child := range entity.children { + system.layout(child, force) + } +} + +func (system *system) draw () { + finalBounds := image.Rectangle { } + + // ignore invalidations that result from drawing elements, because if an + // element decides to do that it really needs to rethink its life + // choices. + system.invalidateIgnore = true + defer func () { system.invalidateIgnore = false } () + + for entity := range system.drawingInvalid { + if entity.clippedBounds.Empty() { continue } + entity.element.Draw (canvas.Cut ( + system.canvas, + entity.clippedBounds)) + finalBounds = finalBounds.Union(entity.clippedBounds) + } + system.drawingInvalid = make(entitySet) + + // TODO: don't just union all the bounds together, we can definetly + // consolidateupdated regions more efficiently than this. + if !finalBounds.Empty() { + system.pushFunc(finalBounds) + } +} diff --git a/backends/x/window.go b/backends/x/window.go index 980d63d..ad39a79 100644 --- a/backends/x/window.go +++ b/backends/x/window.go @@ -13,20 +13,16 @@ import "github.com/jezek/xgbutil/mousebind" import "github.com/jezek/xgbutil/xgraphics" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" -import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/canvas" -// import "runtime/debug" type mainWindow struct { *window } type menuWindow struct { *window } type window struct { + system + backend *Backend xWindow *xwindow.Window xCanvas *xgraphics.Image - canvas canvas.BasicCanvas - child tomo.Element - onClose func () - skipChildDrawCallback bool title, application string @@ -34,15 +30,14 @@ type window struct { hasModal bool shy bool - theme tomo.Theme - config tomo.Config - selectionRequest *selectionRequest selectionClaim *selectionClaim metrics struct { bounds image.Rectangle } + + onClose func () } func (backend *Backend) NewWindow ( @@ -53,6 +48,7 @@ func (backend *Backend) NewWindow ( ) { if backend == nil { panic("nil backend") } window, err := backend.newWindow(bounds, false) + output = mainWindow { window } return output, err } @@ -69,6 +65,10 @@ func (backend *Backend) newWindow ( window := &window { backend: backend } + window.system.initialize() + window.system.pushFunc = window.pasteAndPush + window.theme.Case = tomo.C("tomo", "window") + window.xWindow, err = xwindow.Generate(backend.connection) if err != nil { return } @@ -126,7 +126,7 @@ func (backend *Backend) newWindow ( window.SetConfig(backend.config) window.metrics.bounds = bounds - window.childMinimumSizeChangeCallback(8, 8) + window.setMinimumSize(8, 8) window.reallocateCanvas() @@ -136,69 +136,31 @@ func (backend *Backend) newWindow ( return } -func (window *window) NotifyMinimumSizeChange (child tomo.Element) { - window.childMinimumSizeChangeCallback(child.MinimumSize()) -} - func (window *window) Window () tomo.Window { return window } -func (window *window) RequestFocus ( - child tomo.Focusable, -) ( - granted bool, -) { - return true -} - -func (window *window) RequestFocusNext (child tomo.Focusable) { - if child, ok := window.child.(tomo.Focusable); ok { - if !child.HandleFocus(input.KeynavDirectionForward) { - child.HandleUnfocus() - } - } -} - -func (window *window) RequestFocusPrevious (child tomo.Focusable) { - if child, ok := window.child.(tomo.Focusable); ok { - if !child.HandleFocus(input.KeynavDirectionBackward) { - child.HandleUnfocus() - } - } -} - func (window *window) Adopt (child tomo.Element) { // disown previous child if window.child != nil { - window.child.SetParent(nil) - window.child.DrawTo(nil, image.Rectangle { }, nil) + window.child.unlink() + window.child = nil } + // adopt new child if child != nil { - // adopt new child - window.child = child - child.SetParent(window) - if newChild, ok := child.(tomo.Themeable); ok { - newChild.SetTheme(window.theme) - } - if newChild, ok := child.(tomo.Configurable); ok { - newChild.SetConfig(window.config) - } - if child != nil { - if !window.childMinimumSizeChangeCallback(child.MinimumSize()) { - window.resizeChildToFit() - window.redrawChildEntirely() - } + childEntity, ok := child.Entity().(*entity) + if ok && childEntity != nil { + window.child = childEntity + childEntity.setWindow(window) + window.setMinimumSize ( + childEntity.minWidth, + childEntity.minHeight) + window.resizeChildToFit() } } } -func (window *window) Child () (child tomo.Element) { - child = window.child - return -} - func (window *window) SetTitle (title string) { window.title = title ewmh.WmNameSet ( @@ -317,43 +279,6 @@ func (window menuWindow) Pin () { // TODO iungrab keyboard and mouse } -func (window *window) grabInput () { - keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id) - mousebind.GrabPointer ( - window.backend.connection, - window.xWindow.Id, - window.backend.connection.RootWin(), 0) -} - -func (window *window) ungrabInput () { - keybind.UngrabKeyboard(window.backend.connection) - mousebind.UngrabPointer(window.backend.connection) -} - -func (window *window) inheritProperties (parent *window) { - window.SetApplicationName(parent.application) -} - -func (window *window) setType (ty string) error { - return ewmh.WmWindowTypeSet ( - window.backend.connection, - window.xWindow.Id, - []string { "_NET_WM_WINDOW_TYPE_" + ty }) -} - -func (window *window) setClientLeader (leader *window) error { - hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id) - if hints == nil { - hints = &icccm.Hints { } - } - hints.Flags |= icccm.HintWindowGroup - hints.WindowGroup = leader.xWindow.Id - return icccm.WmHintsSet ( - window.backend.connection, - window.xWindow.Id, - hints) -} - func (window *window) Show () { if window.child == nil { window.xCanvas.For (func (x, y int) xgraphics.BGRA { @@ -362,7 +287,7 @@ func (window *window) Show () { window.pushRegion(window.xCanvas.Bounds()) } - + window.xWindow.Map() if window.shy { window.grabInput() } } @@ -417,18 +342,41 @@ func (window *window) OnClose (callback func ()) { window.onClose = callback } -func (window *window) SetTheme (theme tomo.Theme) { - window.theme = theme - if child, ok := window.child.(tomo.Themeable); ok { - child.SetTheme(theme) - } +func (window *window) grabInput () { + keybind.GrabKeyboard(window.backend.connection, window.xWindow.Id) + mousebind.GrabPointer ( + window.backend.connection, + window.xWindow.Id, + window.backend.connection.RootWin(), 0) } -func (window *window) SetConfig (config tomo.Config) { - window.config = config - if child, ok := window.child.(tomo.Configurable); ok { - child.SetConfig(config) +func (window *window) ungrabInput () { + keybind.UngrabKeyboard(window.backend.connection) + mousebind.UngrabPointer(window.backend.connection) +} + +func (window *window) inheritProperties (parent *window) { + window.SetApplicationName(parent.application) +} + +func (window *window) setType (ty string) error { + return ewmh.WmWindowTypeSet ( + window.backend.connection, + window.xWindow.Id, + []string { "_NET_WM_WINDOW_TYPE_" + ty }) +} + +func (window *window) setClientLeader (leader *window) error { + hints, _ := icccm.WmHintsGet(window.backend.connection, window.xWindow.Id) + if hints == nil { + hints = &icccm.Hints { } } + hints.Flags |= icccm.HintWindowGroup + hints.WindowGroup = leader.xWindow.Id + return icccm.WmHintsSet ( + window.backend.connection, + window.xWindow.Id, + hints) } func (window *window) reallocateCanvas () { @@ -464,22 +412,7 @@ func (window *window) reallocateCanvas () { } -func (window *window) redrawChildEntirely () { - window.paste(window.canvas.Bounds()) - window.pushRegion(window.canvas.Bounds()) -} - -func (window *window) resizeChildToFit () { - window.skipChildDrawCallback = true - window.child.DrawTo ( - window.canvas, - window.canvas.Bounds(), - window.childDrawCallback) - window.skipChildDrawCallback = false -} - -func (window *window) childDrawCallback (region image.Rectangle) { - if window.skipChildDrawCallback { return } +func (window *window) pasteAndPush (region image.Rectangle) { window.paste(region) window.pushRegion(region) } @@ -492,7 +425,6 @@ func (window *window) paste (region image.Rectangle) { dstStride := window.xCanvas.Stride dstData := window.xCanvas.Pix - // debug.PrintStack() for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { srcYComponent := y * stride dstYComponent := y * dstStride @@ -507,7 +439,21 @@ func (window *window) paste (region image.Rectangle) { } } -func (window *window) childMinimumSizeChangeCallback (width, height int) (resized bool) { +func (window *window) pushRegion (region image.Rectangle) { + if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } + image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) + if ok { + image.XDraw() + image.XExpPaint ( + window.xWindow.Id, + image.Bounds().Min.X, + image.Bounds().Min.Y) + } +} + +func (window *window) setMinimumSize (width, height int) { + if width < 8 { width = 8 } + if height < 8 { height = 8 } icccm.WmNormalHintsSet ( window.backend.connection, window.xWindow.Id, @@ -523,20 +469,5 @@ func (window *window) childMinimumSizeChangeCallback (width, height int) (resize if newWidth != window.metrics.bounds.Dx() || newHeight != window.metrics.bounds.Dy() { window.xWindow.Resize(newWidth, newHeight) - return true - } - - return false -} - -func (window *window) pushRegion (region image.Rectangle) { - if window.xCanvas == nil { panic("whoopsie!!!!!!!!!!!!!!") } - image, ok := window.xCanvas.SubImage(region).(*xgraphics.Image) - if ok { - image.XDraw() - image.XExpPaint ( - window.xWindow.Id, - image.Bounds().Min.X, - image.Bounds().Min.Y) } } diff --git a/backends/x/x.go b/backends/x/x.go index 40c3021..ec69708 100644 --- a/backends/x/x.go +++ b/backends/x/x.go @@ -67,6 +67,9 @@ func (backend *Backend) Run () (err error) { <- pingAfter case callback := <- backend.doChannel: callback() + for _, window := range backend.windows { + window.system.afterEvent() + } case <- pingQuit: return } diff --git a/default/theme/default.go b/default/theme/default.go index 74ef9de..1cb4c69 100644 --- a/default/theme/default.go +++ b/default/theme/default.go @@ -212,14 +212,9 @@ func (Default) Pattern (id tomo.Pattern, state tomo.State, c tomo.Case) artist.P switch id { case tomo.PatternBackground: return patterns.Uhex(0xaaaaaaFF) case tomo.PatternDead: return defaultTextures[0][offset] - case tomo.PatternRaised: - if c.Match("tomo", "listEntry", "") { - return defaultTextures[10][offset] - } else { - return defaultTextures[1][offset] - } - case tomo.PatternSunken: return defaultTextures[2][offset] - case tomo.PatternPinboard: return defaultTextures[3][offset] + case tomo.PatternRaised: return defaultTextures[1][offset] + case tomo.PatternSunken: return defaultTextures[2][offset] + case tomo.PatternPinboard: return defaultTextures[3][offset] case tomo.PatternButton: switch { case c.Match("tomo", "checkbox", ""): @@ -272,16 +267,8 @@ func (Default) Color (id tomo.Color, state tomo.State, c tomo.Case) color.RGBA { // Padding returns the default padding value for the given pattern. func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset { switch id { - case tomo.PatternRaised: - if c.Match("tomo", "listEntry", "") { - return artist.I(4, 8) - } else { - return artist.I(8) - } case tomo.PatternSunken: - if c.Match("tomo", "list", "") { - return artist.I(4, 0, 3) - } else if c.Match("tomo", "progressBar", "") { + if c.Match("tomo", "progressBar", "") { return artist.I(2, 1, 1, 2) } else { return artist.I(8) @@ -292,16 +279,26 @@ func (Default) Padding (id tomo.Pattern, c tomo.Case) artist.Inset { } else { return artist.I(8) } + case tomo.PatternTableCell: return artist.I(5) + case tomo.PatternTableHead: return artist.I(5) case tomo.PatternGutter: return artist.I(0) case tomo.PatternLine: return artist.I(1) case tomo.PatternMercury: return artist.I(5) - default: return artist.I(8) + default: return artist.I(8) } } // Margin returns the default margin value for the given pattern. func (Default) Margin (id tomo.Pattern, c tomo.Case) image.Point { - return image.Pt(8, 8) + switch id { + case tomo.PatternSunken: + if c.Match("tomo", "list", "") { + return image.Pt(-1, -1) + } else { + return image.Pt(8, 8) + } + default: return image.Pt(8, 8) + } } // Hints returns rendering optimization hints for a particular pattern. diff --git a/element.go b/element.go index b86a876..cad0a80 100644 --- a/element.go +++ b/element.go @@ -6,54 +6,75 @@ import "git.tebibyte.media/sashakoshka/tomo/canvas" // Element represents a basic on-screen object. type Element interface { - // Bounds reports the element's bounding box. This must reflect the - // bounding last given to the element by DrawTo. - Bounds () image.Rectangle - - // MinimumSize specifies the minimum amount of pixels this element's - // width and height may be set to. If the element is given a resize - // event with dimensions smaller than this, it will use its minimum - // instead of the offending dimension(s). - MinimumSize () (width, height int) + // Draw causes the element to draw to the specified canvas. The bounds + // of this canvas specify the area that is actually drawn to, while the + // Entity bounds specify the actual area of the element. + Draw (canvas.Canvas) - // SetParent sets the parent container of the element. This should only - // be called by the parent when the element is adopted. If parent is set - // to nil, it will mark itself as not having a parent. If this method is - // passed a non-nil value and the element already has a parent, it will - // panic. - SetParent (Parent) - - // DrawTo gives the element a canvas to draw on, along with a bounding - // box to be used for laying out the element. This should only be called - // by the parent element. This is typically a region of the parent - // element's canvas. - DrawTo (canvas canvas.Canvas, bounds image.Rectangle, onDamage func (region image.Rectangle)) + // Entity returns this element's entity. + Entity () Entity } -// Focusable represents an element that has keyboard navigation support. This -// includes inputs, buttons, sliders, etc. as well as any elements that have -// children (so keyboard navigation events can be propagated downward). -type Focusable interface { +// Layoutable represents an element that needs to perform layout calculations +// before it can draw itself. +type Layoutable interface { + Element + + // Layout causes this element to perform a layout operation. + Layout () +} + +// Container represents an element capable of containing child elements. +type Container interface { + Element + Layoutable + + // DrawBackground causes the element to draw its background pattern to + // the specified canvas. The bounds of this canvas specify the area that + // is actually drawn to, while the Entity bounds specify the actual area + // of the element. + DrawBackground (canvas.Canvas) + + // HandleChildMinimumSizeChange is called when a child's minimum size is + // changed. + HandleChildMinimumSizeChange (child Element) +} + +// Enableable represents an element that can be enabled and disabled. Disabled +// elements typically appear greyed out. +type Enableable interface { Element - // Focused returns whether or not this element or any of its children - // are currently focused. - Focused () bool + // Enabled returns whether or not the element is enabled. + Enabled () bool + + // SetEnabled sets whether or not the element is enabled. + SetEnabled (bool) +} - // Focus focuses this element, if its parent element grants the - // request. - Focus () +// Focusable represents an element that has keyboard navigation support. +type Focusable interface { + Element + Enableable - // HandleFocus causes this element to mark itself as focused. If the - // element does not have children, it is disabled, or there are no more - // selectable children in the given direction, it should return false - // and do nothing. Otherwise, it should select itself and any children - // (if applicable) and return true. - HandleFocus (direction input.KeynavDirection) (accepted bool) + // HandleFocusChange is called when the element is focused or unfocused. + HandleFocusChange () +} - // HandleDeselection causes this element to mark itself and all of its - // children as unfocused. - HandleUnfocus () +// Selectable represents an element that can be selected. This includes things +// like list items, files, etc. The difference between this and Focusable is +// that multiple Selectable elements may be selected at the same time, whereas +// only one Focusable element may be focused at the same time. Containers who's +// purpose is to contain selectable elements can determine when to select them +// by implementing MouseTargetContainer and listening for HandleChildMouseDown +// events. +type Selectable interface { + Element + Enableable + + // HandleSelectionChange is called when the element is selected or + // deselected. + HandleSelectionChange () } // KeyboardTarget represents an element that can receive keyboard input. @@ -85,6 +106,22 @@ type MouseTarget interface { HandleMouseUp (x, y int, button input.Button) } +// MouseTargetContainer represents an element that wants to know when one +// of its children is clicked. Children do not have to implement MouseTarget for +// a container satisfying MouseTargetContainer to be notified that they have +// been clicked. +type MouseTargetContainer interface { + Container + + // HandleMouseDown is called when a mouse button is pressed down on a + // child element. + HandleChildMouseDown (x, y int, button input.Button, child Element) + + // HandleMouseUp is called when a mouse button is released that was + // originally pressed down on a child element. + HandleChildMouseUp (x, y int, button input.Button, child Element) +} + // MotionTarget represents an element that can receive mouse motion events. type MotionTarget interface { Element @@ -125,6 +162,16 @@ type Flexible interface { FlexibleHeightFor (width int) int } +// FlexibleContainer represents an element that is capable of containing +// flexible children. +type FlexibleContainer interface { + Container + + // HandleChildFlexibleHeightChange is called when the parameters + // affecting a child's flexible height are changed. + HandleChildFlexibleHeightChange (child Flexible) +} + // Scrollable represents an element that can be scrolled. It acts as a viewport // through which its contents can be observed. type Scrollable interface { @@ -145,6 +192,16 @@ type Scrollable interface { ScrollAxes () (horizontal, vertical bool) } +// ScrollableContainer represents an element that is capable of containing +// scrollable children. +type ScrollableContainer interface { + Container + + // HandleChildScrollBoundsChange is called when the content bounds, + // viewport bounds, or scroll axes of a child are changed. + HandleChildScrollBoundsChange (child Scrollable) +} + // Collapsible represents an element who's minimum width and height can be // manually resized. Scrollable elements should implement this if possible. type Collapsible interface { diff --git a/elements/box.go b/elements/box.go new file mode 100644 index 0000000..dab9117 --- /dev/null +++ b/elements/box.go @@ -0,0 +1,216 @@ +package elements + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +// Space is a list of spacing configurations that can be passed to some +// containers. +type Space int; const ( + SpaceNone = 0 + SpacePadding = 1 + SpaceMargin = 2 + SpaceBoth = SpacePadding | SpaceMargin +) + +// Includes returns whether a spacing value has been or'd with another spacing +// value. +func (space Space) Includes (sub Space) bool { + return (space & sub) > 0 +} + +// Box is a container that lays out its children horizontally or vertically. +// Child elements can be set to contract to their minimum size, or expand to +// fill remaining space. Boxes can be nested and used together to create more +// complex layouts. +type Box struct { + container + theme theme.Wrapped + padding bool + margin bool + vertical bool +} + +// NewHBox creates a new horizontal box. +func NewHBox (space Space, children ...tomo.Element) (element *Box) { + element = &Box { + padding: space.Includes(SpacePadding), + margin: space.Includes(SpaceMargin), + } + element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.minimumSize = element.updateMinimumSize + element.init() + element.theme.Case = tomo.C("tomo", "box") + element.Adopt(children...) + return +} + +// NewHBox creates a new vertical box. +func NewVBox (space Space, children ...tomo.Element) (element *Box) { + element = &Box { + padding: space.Includes(SpacePadding), + margin: space.Includes(SpaceMargin), + vertical: true, + } + element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + element.minimumSize = element.updateMinimumSize + element.init() + element.theme.Case = tomo.C("tomo", "box") + element.Adopt(children...) + return +} + +func (element *Box) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() + } + + tiles := shatter.Shatter(element.entity.Bounds(), rocks...) + for _, tile := range tiles { + element.entity.DrawBackground(canvas.Cut(destination, tile)) + } +} + +func (element *Box) Layout () { + margin := element.theme.Margin(tomo.PatternBackground) + padding := element.theme.Padding(tomo.PatternBackground) + bounds := element.entity.Bounds() + if element.padding { bounds = padding.Apply(bounds) } + + var marginSize float64; if element.vertical { + marginSize = float64(margin.Y) + } else { + marginSize = float64(margin.X) + } + + freeSpace, nExpanding := element.freeSpace() + expandingElementSize := freeSpace / nExpanding + + // set the size and position of each element + x := float64(bounds.Min.X) + y := float64(bounds.Min.Y) + for index := 0; index < element.entity.CountChildren(); index ++ { + entry := element.scratch[element.entity.Child(index)] + + var size float64; if entry.expand { + size = expandingElementSize + } else { + size = entry.minSize + } + + var childBounds image.Rectangle; if element.vertical { + childBounds = tomo.Bounds(int(x), int(y), bounds.Dx(), int(size)) + } else { + childBounds = tomo.Bounds(int(x), int(y), int(size), bounds.Dy()) + } + element.entity.PlaceChild(index, childBounds) + + if element.vertical { + y += size + if element.margin { y += marginSize } + } else { + x += size + if element.margin { x += marginSize } + } + } +} + +func (element *Box) AdoptExpand (children ...tomo.Element) { + element.adopt(true, children...) +} + +func (element *Box) DrawBackground (destination canvas.Canvas) { + element.entity.DrawBackground(destination) +} + +// SetTheme sets the element's theme. +func (element *Box) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Box) freeSpace () (space float64, nExpanding float64) { + margin := element.theme.Margin(tomo.PatternBackground) + padding := element.theme.Padding(tomo.PatternBackground) + + var marginSize int; if element.vertical { + marginSize = margin.Y + } else { + marginSize = margin.X + } + + if element.vertical { + space = float64(element.entity.Bounds().Dy()) + } else { + space = float64(element.entity.Bounds().Dx()) + } + + for _, entry := range element.scratch { + if entry.expand { + nExpanding ++; + } else { + space -= float64(entry.minSize) + } + } + + if element.padding { + space -= float64(padding.Vertical()) + } + if element.margin { + space -= float64(marginSize * (len(element.scratch) - 1)) + } + + return +} + +func (element *Box) updateMinimumSize () { + margin := element.theme.Margin(tomo.PatternBackground) + padding := element.theme.Padding(tomo.PatternBackground) + var breadth, size int + var marginSize int; if element.vertical { + marginSize = margin.Y + } else { + marginSize = margin.X + } + + for index := 0; index < element.entity.CountChildren(); index ++ { + childWidth, childHeight := element.entity.ChildMinimumSize(index) + var childBreadth, childSize int; if element.vertical { + childBreadth, childSize = childWidth, childHeight + } else { + childBreadth, childSize = childHeight, childWidth + } + + key := element.entity.Child(index) + entry := element.scratch[key] + entry.minSize = float64(childSize) + element.scratch[key] = entry + + if childBreadth > breadth { + breadth = childBreadth + } + size += childSize + if element.margin && index > 0 { + size += marginSize + } + } + + var width, height int; if element.vertical { + width, height = breadth, size + } else { + width, height = size, breadth + } + + if element.padding { + width += padding.Horizontal() + height += padding.Vertical() + } + + element.entity.SetMinimumSize(width, height) +} diff --git a/elements/button.go b/elements/button.go index 09946f2..36552d9 100644 --- a/elements/button.go +++ b/elements/button.go @@ -1,24 +1,19 @@ package elements import "image" -// import "runtime/debug" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" -// import "git.tebibyte.media/sashakoshka/tomo/artist" -// import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" // Button is a clickable button. type Button struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity tomo.FocusableEntity drawer textdraw.Drawer + enabled bool pressed bool text string @@ -34,11 +29,9 @@ type Button struct { // NewButton creates a new button with the specified label text. func NewButton (text string) (element *Button) { - element = &Button { showText: true } + element = &Button { showText: true, enabled: true } + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.theme.Case = tomo.C("tomo", "button") - element.Core, element.core = core.NewCore(element, element.drawAll) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) @@ -46,163 +39,19 @@ func NewButton (text string) (element *Button) { return } -func (element *Button) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - if !element.Focused() { element.Focus() } - if button != input.ButtonLeft { return } - element.pressed = true - element.drawAndPush() +// Entity returns this element's entity. +func (element *Button) Entity () tomo.Entity { + return element.entity } -func (element *Button) HandleMouseUp (x, y int, button input.Button) { - if button != input.ButtonLeft { return } - element.pressed = false - within := image.Point { x, y }. - In(element.Bounds()) - if element.Enabled() && within && element.onClick != nil { - element.onClick() - } - element.drawAndPush() -} - -func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if !element.Enabled() { return } - if key == input.KeyEnter { - element.pressed = true - element.drawAndPush() - } -} - -func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter && element.pressed { - element.pressed = false - element.drawAndPush() - if !element.Enabled() { return } - if element.onClick != nil { - element.onClick() - } - } -} - -// OnClick sets the function to be called when the button is clicked. -func (element *Button) OnClick (callback func ()) { - element.onClick = callback -} - -// SetEnabled sets whether this button can be clicked or not. -func (element *Button) SetEnabled (enabled bool) { - element.focusableControl.SetEnabled(enabled) -} - -// SetText sets the button's label text. -func (element *Button) SetText (text string) { - if element.text == text { return } - - element.text = text - element.drawer.SetText([]rune(text)) - element.updateMinimumSize() - element.drawAndPush() -} - -// SetIcon sets the icon of the button. Passing theme.IconNone removes the -// current icon if it exists. -func (element *Button) SetIcon (id tomo.Icon) { - if id == tomo.IconNone { - element.hasIcon = false - element.updateMinimumSize() - element.drawAndPush() - } else { - if element.hasIcon && element.iconId == id { return } - element.hasIcon = true - element.iconId = id - } - element.updateMinimumSize() - element.drawAndPush() -} - -// ShowText sets whether or not the button's text will be displayed. -func (element *Button) ShowText (showText bool) { - if element.showText == showText { return } - element.showText = showText - element.updateMinimumSize() - element.drawAndPush() -} - -// SetTheme sets the element's theme. -func (element *Button) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.drawAndPush() -} - -// SetConfig sets the element's configuration. -func (element *Button) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - element.updateMinimumSize() - element.drawAndPush() -} - -func (element *Button) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternButton) - margin := element.theme.Margin(tomo.PatternButton) - - textBounds := element.drawer.LayoutBounds() - minimumSize := textBounds.Sub(textBounds.Min) - - if element.hasIcon { - icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) - if icon != nil { - bounds := icon.Bounds() - if element.showText { - minimumSize.Max.X += bounds.Dx() - minimumSize.Max.X += margin.X - } else { - minimumSize.Max.X = bounds.Dx() - } - } - } - - minimumSize = padding.Inverse().Apply(minimumSize) - element.core.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) -} - -func (element *Button) state () tomo.State { - return tomo.State { - Disabled: !element.Enabled(), - Focused: element.Focused(), - Pressed: element.pressed, - } -} - -func (element *Button) drawAndPush () { - if element.core.HasImage () { - element.drawAll() - element.core.DamageAll() - } -} - -func (element *Button) drawAll () { - element.drawBackground() - element.drawText() -} - -func (element *Button) drawBackground () []image.Rectangle { +// Draw causes the element to draw to the specified destination canvas. +func (element *Button) Draw (destination canvas.Canvas) { state := element.state() - bounds := element.Bounds() + bounds := element.entity.Bounds() pattern := element.theme.Pattern(tomo.PatternButton, state) - pattern.Draw(element.core, bounds) - return []image.Rectangle { bounds } -} - -func (element *Button) drawText () { - state := element.state() - bounds := element.Bounds() + pattern.Draw(destination, bounds) + foreground := element.theme.Color(tomo.ColorForeground, state) sink := element.theme.Sink(tomo.PatternButton) margin := element.theme.Margin(tomo.PatternButton) @@ -240,7 +89,7 @@ func (element *Button) drawText () { } offset.X += addedWidth / 2 - icon.Draw(element.core, foreground, iconOffset) + icon.Draw(destination, foreground, iconOffset) } } @@ -248,6 +97,151 @@ func (element *Button) drawText () { if element.pressed { offset = offset.Add(sink) } - element.drawer.Draw(element.core, foreground, offset) + element.drawer.Draw(destination, foreground, offset) + } +} + +// OnClick sets the function to be called when the button is clicked. +func (element *Button) OnClick (callback func ()) { + element.onClick = callback +} + +// Focus gives this element input focus. +func (element *Button) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this button is enabled or not. +func (element *Button) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this button can be clicked or not. +func (element *Button) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + +// SetText sets the button's label text. +func (element *Button) SetText (text string) { + if element.text == text { return } + element.text = text + element.drawer.SetText([]rune(text)) + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetIcon sets the icon of the button. Passing theme.IconNone removes the +// current icon if it exists. +func (element *Button) SetIcon (id tomo.Icon) { + if id == tomo.IconNone { + element.hasIcon = false + } else { + if element.hasIcon && element.iconId == id { return } + element.hasIcon = true + element.iconId = id + } + element.updateMinimumSize() + element.entity.Invalidate() +} + +// ShowText sets whether or not the button's text will be displayed. +func (element *Button) ShowText (showText bool) { + if element.showText == showText { return } + element.showText = showText + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetTheme sets the element's theme. +func (element *Button) SetTheme (new tomo.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.drawer.SetFace (element.theme.FontFace ( + tomo.FontStyleRegular, + tomo.FontSizeNormal)) + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetConfig sets the element's configuration. +func (element *Button) SetConfig (new tomo.Config) { + if new == element.config.Config { return } + element.config.Config = new + element.updateMinimumSize() + element.entity.Invalidate() +} + +func (element *Button) HandleFocusChange () { + element.entity.Invalidate() +} + +func (element *Button) HandleMouseDown (x, y int, button input.Button) { + if !element.Enabled() { return } + element.Focus() + if button != input.ButtonLeft { return } + element.pressed = true + element.entity.Invalidate() +} + +func (element *Button) HandleMouseUp (x, y int, button input.Button) { + if button != input.ButtonLeft { return } + element.pressed = false + within := image.Point { x, y }.In(element.entity.Bounds()) + if element.Enabled() && within && element.onClick != nil { + element.onClick() + } + element.entity.Invalidate() +} + +func (element *Button) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + if !element.Enabled() { return } + if key == input.KeyEnter { + element.pressed = true + element.entity.Invalidate() + } +} + +func (element *Button) HandleKeyUp(key input.Key, modifiers input.Modifiers) { + if key == input.KeyEnter && element.pressed { + element.pressed = false + element.entity.Invalidate() + if !element.Enabled() { return } + if element.onClick != nil { + element.onClick() + } + } +} + +func (element *Button) updateMinimumSize () { + padding := element.theme.Padding(tomo.PatternButton) + margin := element.theme.Margin(tomo.PatternButton) + + textBounds := element.drawer.LayoutBounds() + minimumSize := textBounds.Sub(textBounds.Min) + + if element.hasIcon { + icon := element.theme.Icon(element.iconId, tomo.IconSizeSmall) + if icon != nil { + bounds := icon.Bounds() + if element.showText { + minimumSize.Max.X += bounds.Dx() + minimumSize.Max.X += margin.X + } else { + minimumSize.Max.X = bounds.Dx() + } + } + } + + minimumSize = padding.Inverse().Apply(minimumSize) + element.entity.SetMinimumSize(minimumSize.Dx(), minimumSize.Dy()) +} + +func (element *Button) state () tomo.State { + return tomo.State { + Disabled: !element.Enabled(), + Focused: element.entity.Focused(), + Pressed: element.pressed, } } diff --git a/elements/cell.go b/elements/cell.go new file mode 100644 index 0000000..f9da0db --- /dev/null +++ b/elements/cell.go @@ -0,0 +1,165 @@ +package elements + +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +type cellEntity interface { + tomo.ContainerEntity + tomo.SelectableEntity +} + +// Cell is a single-element container that satisfies tomo.Selectable. It +// provides styling based on whether or not it is selected. +type Cell struct { + entity cellEntity + child tomo.Element + enabled bool + theme theme.Wrapped + + onSelectionChange func () +} + +// NewCell creates a new cell element. If padding is true, the cell will have +// padding on all sides. Child can be nil and added later with the Adopt() +// method. +func NewCell (child tomo.Element) (element *Cell) { + element = &Cell { enabled: true } + element.theme.Case = tomo.C("tomo", "cell") + element.entity = tomo.NewEntity(element).(cellEntity) + element.Adopt(child) + return +} + +// Entity returns this element's entity. +func (element *Cell) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Cell) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + pattern := element.theme.Pattern(tomo.PatternTableCell, element.state()) + if element.child == nil { + pattern.Draw(destination, bounds) + } else { + artist.DrawShatter ( + destination, pattern, bounds, + element.child.Entity().Bounds()) + } +} + +// Draw causes the element to perform a layout operation. +func (element *Cell) Layout () { + if element.child == nil { return } + + bounds := element.entity.Bounds() + bounds = element.theme.Padding(tomo.PatternTableCell).Apply(bounds) + + element.entity.PlaceChild(0, bounds) +} + +// DrawBackground draws this element's background pattern to the specified +// destination canvas. +func (element *Cell) DrawBackground (destination canvas.Canvas) { + element.theme.Pattern(tomo.PatternTableCell, element.state()). + Draw(destination, element.entity.Bounds()) +} + +// Adopt sets this element's child. If nil is passed, any child is removed. +func (element *Cell) Adopt (child tomo.Element) { + if element.child != nil { + element.entity.Disown(element.entity.IndexOf(element.child)) + } + if child != nil { + element.entity.Adopt(child) + } + element.child = child + + element.updateMinimumSize() + element.entity.Invalidate() + element.invalidateChild() + element.entity.InvalidateLayout() +} + +// Child returns this element's child. If there is no child, this method will +// return nil. +func (element *Cell) Child () tomo.Element { + return element.child +} + +// Enabled returns whether this cell is enabled or not. +func (element *Cell) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this cell can be selected or not. +func (element *Cell) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() + element.invalidateChild() +} + +// SetTheme sets this element's theme. +func (element *Cell) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.invalidateChild() + element.entity.InvalidateLayout() +} + +// OnSelectionChange sets a function to be called when this element is selected +// or unselected. +func (element *Cell) OnSelectionChange (callback func ()) { + element.onSelectionChange = callback +} + +func (element *Cell) Selected () bool { + return element.entity.Selected() +} + +func (element *Cell) HandleSelectionChange () { + element.entity.Invalidate() + element.invalidateChild() + if element.onSelectionChange != nil { + element.onSelectionChange() + } +} + +func (element *Cell) HandleChildMinimumSizeChange (tomo.Element) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Cell) state () tomo.State { + return tomo.State { + Disabled: !element.enabled, + On: element.entity.Selected(), + } +} + +func (element *Cell) updateMinimumSize () { + width, height := 0, 0 + + if element.child != nil { + childWidth, childHeight := element.entity.ChildMinimumSize(0) + width += childWidth + height += childHeight + } + padding := element.theme.Padding(tomo.PatternTableCell) + width += padding.Horizontal() + height += padding.Vertical() + + element.entity.SetMinimumSize(width, height) +} + +func (element *Cell) invalidateChild () { + if element.child != nil { + element.child.Entity().Invalidate() + } +} diff --git a/elements/checkbox.go b/elements/checkbox.go index f90b90d..9ae8295 100644 --- a/elements/checkbox.go +++ b/elements/checkbox.go @@ -3,19 +3,17 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Checkbox is a toggle-able checkbox with a label. type Checkbox struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity tomo.FocusableEntity drawer textdraw.Drawer + enabled bool pressed bool checked bool text string @@ -28,11 +26,9 @@ type Checkbox struct { // NewCheckbox creates a new cbeckbox with the specified label text. func NewCheckbox (text string, checked bool) (element *Checkbox) { - element = &Checkbox { checked: checked } + element = &Checkbox { checked: checked, enabled: true } + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.theme.Case = tomo.C("tomo", "checkbox") - element.Core, element.core = core.NewCore(element, element.draw) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.redo) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) @@ -40,57 +36,39 @@ func NewCheckbox (text string, checked bool) (element *Checkbox) { return } -func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - element.Focus() - element.pressed = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } +// Entity returns this element's entity. +func (element *Checkbox) Entity () tomo.Entity { + return element.entity } -func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) { - if button != input.ButtonLeft || !element.pressed { return } +// Draw causes the element to draw to the specified destination canvas. +func (element *Checkbox) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) - element.pressed = false - within := image.Point { x, y }. - In(element.Bounds()) - if within { - element.checked = !element.checked + state := tomo.State { + Disabled: !element.Enabled(), + Focused: element.entity.Focused(), + Pressed: element.pressed, + On: element.checked, } - - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - if within && element.onToggle != nil { - element.onToggle() - } -} -func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter { - element.pressed = true - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - } -} + element.entity.DrawBackground(destination) + + pattern := element.theme.Pattern(tomo.PatternButton, state) + pattern.Draw(destination, boxBounds) -func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter && element.pressed { - element.pressed = false - element.checked = !element.checked - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - if element.onToggle != nil { - element.onToggle() - } - } + textBounds := element.drawer.LayoutBounds() + margin := element.theme.Margin(tomo.PatternBackground) + offset := bounds.Min.Add(image.Point { + X: bounds.Dy() + margin.X, + }) + + offset.Y -= textBounds.Min.Y + offset.X -= textBounds.Min.X + + foreground := element.theme.Color(tomo.ColorForeground, state) + element.drawer.Draw(destination, foreground, offset) } // OnToggle sets the function to be called when the checkbox is toggled. @@ -103,23 +81,30 @@ func (element *Checkbox) Value () (checked bool) { return element.checked } +// Focus gives this element input focus. +func (element *Checkbox) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this checkbox is enabled or not. +func (element *Checkbox) Enabled () bool { + return element.enabled +} + // SetEnabled sets whether this checkbox can be toggled or not. func (element *Checkbox) SetEnabled (enabled bool) { - element.focusableControl.SetEnabled(enabled) + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() } // SetText sets the checkbox's label text. func (element *Checkbox) SetText (text string) { if element.text == text { return } - element.text = text element.drawer.SetText([]rune(text)) element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -130,7 +115,7 @@ func (element *Checkbox) SetTheme (new tomo.Theme) { tomo.FontStyleRegular, tomo.FontSizeNormal)) element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -138,54 +123,61 @@ func (element *Checkbox) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() +} + +func (element *Checkbox) HandleFocusChange () { + element.entity.Invalidate() +} + +func (element *Checkbox) HandleMouseDown (x, y int, button input.Button) { + if !element.Enabled() { return } + element.Focus() + element.pressed = true + element.entity.Invalidate() +} + +func (element *Checkbox) HandleMouseUp (x, y int, button input.Button) { + if button != input.ButtonLeft || !element.pressed { return } + + element.pressed = false + within := image.Point { x, y }.In(element.entity.Bounds()) + if within { + element.checked = !element.checked + } + + element.entity.Invalidate() + if within && element.onToggle != nil { + element.onToggle() + } +} + +func (element *Checkbox) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + if key == input.KeyEnter { + element.pressed = true + element.entity.Invalidate() + } +} + +func (element *Checkbox) HandleKeyUp (key input.Key, modifiers input.Modifiers) { + if key == input.KeyEnter && element.pressed { + element.pressed = false + element.checked = !element.checked + element.entity.Invalidate() + if element.onToggle != nil { + element.onToggle() + } + } } func (element *Checkbox) updateMinimumSize () { textBounds := element.drawer.LayoutBounds() if element.text == "" { - element.core.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) + element.entity.SetMinimumSize(textBounds.Dy(), textBounds.Dy()) } else { margin := element.theme.Margin(tomo.PatternBackground) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( textBounds.Dy() + margin.X + textBounds.Dx(), textBounds.Dy()) } } - -func (element *Checkbox) redo () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *Checkbox) draw () { - bounds := element.Bounds() - boxBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) - - state := tomo.State { - Disabled: !element.Enabled(), - Focused: element.Focused(), - Pressed: element.pressed, - On: element.checked, - } - - element.core.DrawBackground ( - element.theme.Pattern(tomo.PatternBackground, state)) - - pattern := element.theme.Pattern(tomo.PatternButton, state) - pattern.Draw(element.core, boxBounds) - - textBounds := element.drawer.LayoutBounds() - margin := element.theme.Margin(tomo.PatternBackground) - offset := bounds.Min.Add(image.Point { - X: bounds.Dy() + margin.X, - }) - - offset.Y -= textBounds.Min.Y - offset.X -= textBounds.Min.X - - foreground := element.theme.Color(tomo.ColorForeground, state) - element.drawer.Draw(element.core, foreground, offset) -} diff --git a/elements/container.go b/elements/container.go new file mode 100644 index 0000000..49d2c88 --- /dev/null +++ b/elements/container.go @@ -0,0 +1,77 @@ +package elements + +import "git.tebibyte.media/sashakoshka/tomo" + +type scratchEntry struct { + expand bool + minSize float64 + minBreadth float64 +} + +type container struct { + entity tomo.ContainerEntity + scratch map[tomo.Element] scratchEntry + minimumSize func () +} + +func (container *container) Entity () tomo.Entity { + return container.entity +} + +func (container *container) Adopt (children ...tomo.Element) { + container.adopt(false, children...) +} + +func (container *container) init () { + container.scratch = make(map[tomo.Element] scratchEntry) +} + +func (container *container) adopt (expand bool, children ...tomo.Element) { + for _, child := range children { + container.entity.Adopt(child) + container.scratch[child] = scratchEntry { expand: expand } + } + container.minimumSize() + container.entity.Invalidate() + container.entity.InvalidateLayout() +} + +func (container *container) Disown (children ...tomo.Element) { + for _, child := range children { + index := container.entity.IndexOf(child) + if index < 0 { continue } + container.entity.Disown(index) + delete(container.scratch, child) + } + container.minimumSize() + container.entity.Invalidate() + container.entity.InvalidateLayout() +} + +func (container *container) DisownAll () { + func () { + for index := 0; index < container.entity.CountChildren(); index ++ { + index := index + defer container.entity.Disown(index) + } + } () + container.scratch = make(map[tomo.Element] scratchEntry) + container.minimumSize() + container.entity.Invalidate() + container.entity.InvalidateLayout() +} + +func (container *container) Child (index int) tomo.Element { + if index < 0 || index >= container.entity.CountChildren() { return nil } + return container.entity.Child(index) +} + +func (container *container) CountChildren () int { + return container.entity.CountChildren() +} + +func (container *container) HandleChildMinimumSizeChange (child tomo.Element) { + container.minimumSize() + container.entity.Invalidate() + container.entity.InvalidateLayout() +} diff --git a/elements/containers/container.go b/elements/containers/container.go deleted file mode 100644 index c10b5ba..0000000 --- a/elements/containers/container.go +++ /dev/null @@ -1,258 +0,0 @@ -package containers - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// Container is an element capable of containg other elements, and arranging -// them in a layout. -type Container struct { - *core.Core - *core.Propagator - core core.CoreControl - - layout tomo.Layout - children []tomo.LayoutEntry - warping bool - - config config.Wrapped - theme theme.Wrapped - - onFocusRequest func () (granted bool) - onFocusMotionRequest func (input.KeynavDirection) (granted bool) -} - -// NewContainer creates a new container. -func NewContainer (layout tomo.Layout) (element *Container) { - element = &Container { } - element.theme.Case = tomo.C("tomo", "container") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - element.SetLayout(layout) - return -} - -// SetLayout sets the layout of this container. -func (element *Container) SetLayout (layout tomo.Layout) { - element.layout = layout - element.updateMinimumSize() - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -// Adopt adds a new child element to the container. If expand is set to true, -// the element will expand (instead of contract to its minimum size), in -// whatever way is defined by the current layout. -func (element *Container) Adopt (child tomo.Element, expand bool) { - if child0, ok := child.(tomo.Themeable); ok { - child0.SetTheme(element.theme.Theme) - } - if child0, ok := child.(tomo.Configurable); ok { - child0.SetConfig(element.config.Config) - } - child.SetParent(element) - - // add child - element.children = append (element.children, tomo.LayoutEntry { - Element: child, - Expand: expand, - }) - - // refresh stale data - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// Warp runs the specified callback, deferring all layout and rendering updates -// until the callback has finished executing. This allows for aplications to -// perform batch gui updates without flickering and stuff. -func (element *Container) Warp (callback func ()) { - if element.warping { - callback() - return - } - - element.warping = true - callback() - element.warping = false - - // TODO: create some sort of task list so we don't do a full recalculate - // and redraw every time, because although that is the most likely use - // case, it is not the only one. - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -// Disown removes the given child from the container if it is contained within -// it. -func (element *Container) Disown (child tomo.Element) { - for index, entry := range element.children { - if entry.Element == child { - element.clearChildEventHandlers(entry.Element) - element.children = append ( - element.children[:index], - element.children[index + 1:]...) - break - } - } - - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -func (element *Container) clearChildEventHandlers (child tomo.Element) { - child.DrawTo(nil, image.Rectangle { }, nil) - child.SetParent(nil) - - if child, ok := child.(tomo.Focusable); ok { - if child.Focused() { - child.HandleUnfocus() - } - } -} - -// DisownAll removes all child elements from the container at once. -func (element *Container) DisownAll () { - for _, entry := range element.children { - element.clearChildEventHandlers(entry.Element) - } - element.children = nil - - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// Children returns a slice containing this element's children. -func (element *Container) Children () (children []tomo.Element) { - children = make([]tomo.Element, len(element.children)) - for index, entry := range element.children { - children[index] = entry.Element - } - return -} - -// CountChildren returns the amount of children contained within this element. -func (element *Container) CountChildren () (count int) { - return len(element.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *Container) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index].Element -} - -// ChildAt returns the child that contains the specified x and y coordinates. If -// there are no children at the coordinates, this method will return nil. -func (element *Container) ChildAt (point image.Point) (child tomo.Element) { - for _, entry := range element.children { - if point.In(entry.Bounds) { - child = entry.Element - } - } - return -} - -func (element *Container) redoAll () { - if !element.core.HasImage() { return } - - // remove child canvasses so that any operations done in here will not - // cause a child to draw to a wack ass canvas. - for _, entry := range element.children { - entry.DrawTo(nil, entry.Bounds, nil) - } - - // do a layout - element.doLayout() - - // draw a background - rocks := make([]image.Rectangle, len(element.children)) - for index, entry := range element.children { - rocks[index] = entry.Bounds - } - - element.core.DrawBackgroundBoundsShatter ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { }), - element.Bounds(), - rocks...) - - // cut our canvas up and give peices to child elements - for _, entry := range element.children { - entry.DrawTo ( - canvas.Cut(element.core, entry.Bounds), - entry.Bounds, func (region image.Rectangle) { - element.core.DamageRegion(region) - }) - } -} - -func (element *Container) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *Container) NotifyMinimumSizeChange (child tomo.Element) { - element.updateMinimumSize() - element.redoAll() - element.core.DamageAll() -} - -// DrawBackground draws a portion of the container's background pattern within -// the specified bounds. The container will not push these changes. -func (element *Container) DrawBackground (bounds image.Rectangle) { - element.core.DrawBackgroundBounds ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { }), - bounds) -} - -// SetTheme sets the element's theme. -func (element *Container) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.Propagator.SetTheme(new) - element.updateMinimumSize() - element.redoAll() -} - -// SetConfig sets the element's configuration. -func (element *Container) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.Propagator.SetConfig(new) - element.updateMinimumSize() - element.redoAll() -} - -func (element *Container) updateMinimumSize () { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) - width, height := element.layout.MinimumSize ( - element.children, margin, padding) - element.core.SetMinimumSize(width, height) -} - -func (element *Container) doLayout () { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) - element.layout.Arrange ( - element.children, margin, - padding, element.Bounds()) -} diff --git a/elements/containers/document.go b/elements/containers/document.go deleted file mode 100644 index 4ba9fde..0000000 --- a/elements/containers/document.go +++ /dev/null @@ -1,368 +0,0 @@ -package containers - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// DocumentContainer is a scrollable container capable of containing flexible -// elements. -type DocumentContainer struct { - *core.Core - *core.Propagator - core core.CoreControl - - children []tomo.LayoutEntry - scroll image.Point - warping bool - contentBounds image.Rectangle - - config config.Wrapped - theme theme.Wrapped - - onScrollBoundsChange func () -} - -// NewDocumentContainer creates a new document container. -func NewDocumentContainer () (element *DocumentContainer) { - element = &DocumentContainer { } - element.theme.Case = tomo.C("tomo", "documentContainer") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - return -} - -// Adopt adds a new child element to the container. If expand is true, then the -// element will stretch to either side of the container (much like a css block -// element). If expand is false, the element will share a line with other inline -// elements. -func (element *DocumentContainer) Adopt (child tomo.Element, expand bool) { - // set event handlers - if child0, ok := child.(tomo.Themeable); ok { - child0.SetTheme(element.theme.Theme) - } - if child0, ok := child.(tomo.Configurable); ok { - child0.SetConfig(element.config.Config) - } - - // add child - element.children = append (element.children, tomo.LayoutEntry { - Element: child, - Expand: expand, - }) - - child.SetParent(element) - - // refresh stale data - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// Warp runs the specified callback, deferring all layout and rendering updates -// until the callback has finished executing. This allows for aplications to -// perform batch gui updates without flickering and stuff. -func (element *DocumentContainer) Warp (callback func ()) { - if element.warping { - callback() - return - } - - element.warping = true - callback() - element.warping = false - - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -// Disown removes the given child from the container if it is contained within -// it. -func (element *DocumentContainer) Disown (child tomo.Element) { - for index, entry := range element.children { - if entry.Element == child { - element.clearChildEventHandlers(entry.Element) - element.children = append ( - element.children[:index], - element.children[index + 1:]...) - break - } - } - - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -func (element *DocumentContainer) clearChildEventHandlers (child tomo.Element) { - child.DrawTo(nil, image.Rectangle { }, nil) - child.SetParent(nil) - - if child, ok := child.(tomo.Focusable); ok { - if child.Focused() { - child.HandleUnfocus() - } - } -} - -// DisownAll removes all child elements from the container at once. -func (element *DocumentContainer) DisownAll () { - for _, entry := range element.children { - element.clearChildEventHandlers(entry.Element) - } - element.children = nil - - element.updateMinimumSize() - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// Children returns a slice containing this element's children. -func (element *DocumentContainer) Children () (children []tomo.Element) { - children = make([]tomo.Element, len(element.children)) - for index, entry := range element.children { - children[index] = entry.Element - } - return -} - -// CountChildren returns the amount of children contained within this element. -func (element *DocumentContainer) CountChildren () (count int) { - return len(element.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *DocumentContainer) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index].Element -} - -// ChildAt returns the child that contains the specified x and y coordinates. If -// there are no children at the coordinates, this method will return nil. -func (element *DocumentContainer) ChildAt (point image.Point) (child tomo.Element) { - for _, entry := range element.children { - if point.In(entry.Bounds) { - child = entry.Element - } - } - return -} - -func (element *DocumentContainer) redoAll () { - if !element.core.HasImage() { return } - - // do a layout - element.doLayout() - - maxScrollHeight := element.maxScrollHeight() - if element.scroll.Y > maxScrollHeight { - element.scroll.Y = maxScrollHeight - element.doLayout() - } - - // draw a background - rocks := make([]image.Rectangle, len(element.children)) - for index, entry := range element.children { - rocks[index] = entry.Bounds - } - pattern := element.theme.Pattern ( - tomo.PatternBackground, - tomo.State { }) - artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...) - - element.partition() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } -} - -func (element *DocumentContainer) partition () { - for _, entry := range element.children { - entry.DrawTo(nil, entry.Bounds, nil) - } - - // cut our canvas up and give peices to child elements - for _, entry := range element.children { - if entry.Bounds.Overlaps(element.Bounds()) { - entry.DrawTo ( - canvas.Cut(element.core, entry.Bounds), - entry.Bounds, func (region image.Rectangle) { - element.core.DamageRegion(region) - }) - } - } -} - -func (element *DocumentContainer) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *DocumentContainer) NotifyMinimumSizeChange (child tomo.Element) { - element.redoAll() - element.core.DamageAll() -} - -// DrawBackground draws a portion of the container's background pattern within -// the specified bounds. The container will not push these changes. -func (element *DocumentContainer) DrawBackground (bounds image.Rectangle) { - element.core.DrawBackgroundBounds ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { }), - bounds) -} - -// NotifyFlexibleHeightChange notifies the parent that the parameters -// affecting a child's flexible height have changed. This method is -// expected to be called by flexible child element when their content -// changes. -func (element *DocumentContainer) NotifyFlexibleHeightChange (child tomo.Flexible) { - element.redoAll() - element.core.DamageAll() -} - -// SetTheme sets the element's theme. -func (element *DocumentContainer) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.Propagator.SetTheme(new) - element.redoAll() -} - -// SetConfig sets the element's configuration. -func (element *DocumentContainer) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.Propagator.SetConfig(new) - element.redoAll() -} - -// ScrollContentBounds returns the full content size of the element. -func (element *DocumentContainer) ScrollContentBounds () image.Rectangle { - return element.contentBounds -} - -// ScrollViewportBounds returns the size and position of the element's -// viewport relative to ScrollBounds. -func (element *DocumentContainer) ScrollViewportBounds () image.Rectangle { - padding := element.theme.Padding(tomo.PatternBackground) - bounds := padding.Apply(element.Bounds()) - bounds = bounds.Sub(bounds.Min).Add(element.scroll) - return bounds -} - -// ScrollTo scrolls the viewport to the specified point relative to -// ScrollBounds. -func (element *DocumentContainer) ScrollTo (position image.Point) { - if position.Y < 0 { - position.Y = 0 - } - maxScrollHeight := element.maxScrollHeight() - if position.Y > maxScrollHeight { - position.Y = maxScrollHeight - } - element.scroll = position - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// OnScrollBoundsChange sets a function to be called when the element's viewport -// bounds, content bounds, or scroll axes change. -func (element *DocumentContainer) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - -func (element *DocumentContainer) maxScrollHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) - viewportHeight := element.Bounds().Dy() - padding.Vertical() - height = element.contentBounds.Dy() - viewportHeight - if height < 0 { height = 0 } - return -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *DocumentContainer) ScrollAxes () (horizontal, vertical bool) { - return false, true -} - -func (element *DocumentContainer) doLayout () { - margin := element.theme.Margin(tomo.PatternBackground) - padding := element.theme.Padding(tomo.PatternBackground) - bounds := padding.Apply(element.Bounds()) - element.contentBounds = image.Rectangle { } - - dot := bounds.Min.Sub(element.scroll) - xStart := dot.X - rowHeight := 0 - - nextLine := func () { - dot.X = xStart - dot.Y += margin.Y - dot.Y += rowHeight - rowHeight = 0 - } - - for index, entry := range element.children { - if dot.X > xStart && entry.Expand { - nextLine() - } - - width, height := entry.MinimumSize() - if width + dot.X > bounds.Dx() && !entry.Expand { - nextLine() - } - if width < bounds.Dx() && entry.Expand { - width = bounds.Dx() - } - if typedChild, ok := entry.Element.(tomo.Flexible); ok { - height = typedChild.FlexibleHeightFor(width) - } - if rowHeight < height { - rowHeight = height - } - - entry.Bounds.Min = dot - entry.Bounds.Max = image.Pt(dot.X + width, dot.Y + height) - element.children[index] = entry - element.contentBounds = element.contentBounds.Union(entry.Bounds) - - if entry.Expand { - nextLine() - } else { - dot.X += width + margin.X - } - } - - element.contentBounds = - element.contentBounds.Sub(element.contentBounds.Min) -} - -func (element *DocumentContainer) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternBackground) - minimumWidth := 0 - for _, entry := range element.children { - width, _ := entry.MinimumSize() - if width > minimumWidth { - minimumWidth = width - } - } - element.core.SetMinimumSize ( - minimumWidth + padding.Horizontal(), - padding.Vertical()) -} diff --git a/elements/containers/scroll.go b/elements/containers/scroll.go deleted file mode 100644 index f523d4b..0000000 --- a/elements/containers/scroll.go +++ /dev/null @@ -1,332 +0,0 @@ -package containers - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// ScrollContainer is a container that is capable of holding a scrollable -// element. -type ScrollContainer struct { - *core.Core - *core.Propagator - core core.CoreControl - - child tomo.Scrollable - horizontal *elements.ScrollBar - vertical *elements.ScrollBar - - config config.Wrapped - theme theme.Wrapped - - onFocusRequest func () (granted bool) - onFocusMotionRequest func (input.KeynavDirection) (granted bool) -} - -// NewScrollContainer creates a new scroll container with the specified scroll -// bars. -func NewScrollContainer (horizontal, vertical bool) (element *ScrollContainer) { - element = &ScrollContainer { } - element.theme.Case = tomo.C("tomo", "scrollContainer") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - - if horizontal { - element.horizontal = elements.NewScrollBar(false) - element.setUpChild(element.horizontal) - element.horizontal.OnScroll (func (viewport image.Point) { - if element.child != nil { - element.child.ScrollTo(viewport) - } - if element.vertical != nil { - element.vertical.SetBounds ( - element.child.ScrollContentBounds(), - element.child.ScrollViewportBounds()) - } - }) - } - if vertical { - element.vertical = elements.NewScrollBar(true) - element.setUpChild(element.vertical) - element.vertical.OnScroll (func (viewport image.Point) { - if element.child != nil { - element.child.ScrollTo(viewport) - } - if element.horizontal != nil { - element.horizontal.SetBounds ( - element.child.ScrollContentBounds(), - element.child.ScrollViewportBounds()) - } - }) - } - return -} - - -// Adopt adds a scrollable element to the scroll container. The container can -// only contain one scrollable element at a time, and when a new one is adopted -// it replaces the last one. -func (element *ScrollContainer) Adopt (child tomo.Scrollable) { - // disown previous child if it exists - if element.child != nil { - element.disownChild(child) - } - - // adopt new child - element.child = child - if child != nil { - element.setUpChild(child) - } - - element.updateEnabled() - element.updateMinimumSize() - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -func (element *ScrollContainer) setUpChild (child tomo.Element) { - child.SetParent(element) - if child, ok := child.(tomo.Themeable); ok { - child.SetTheme(element.theme.Theme) - } - if child, ok := child.(tomo.Configurable); ok { - child.SetConfig(element.config.Config) - } -} - -func (element *ScrollContainer) disownChild (child tomo.Scrollable) { - child.DrawTo(nil, image.Rectangle { }, nil) - child.SetParent(nil) - if child, ok := child.(tomo.Focusable); ok { - if child.Focused() { - child.HandleUnfocus() - } - } -} - -func (element *ScrollContainer) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *ScrollContainer) NotifyMinimumSizeChange (child tomo.Element) { - element.redoAll() - element.core.DamageAll() -} - -// NotifyScrollBoundsChange notifies the container that the scroll bounds or -// axes of a child have changed. -func (element *ScrollContainer) NotifyScrollBoundsChange (child tomo.Scrollable) { - element.updateEnabled() - viewportBounds := element.child.ScrollViewportBounds() - contentBounds := element.child.ScrollContentBounds() - if element.horizontal != nil { - element.horizontal.SetBounds(contentBounds, viewportBounds) - } - if element.vertical != nil { - element.vertical.SetBounds(contentBounds, viewportBounds) - } -} - -// DrawBackground draws a portion of the container's background pattern within -// the specified bounds. The container will not push these changes. -func (element *ScrollContainer) DrawBackground (bounds image.Rectangle) { - element.core.DrawBackgroundBounds ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { }), - bounds) -} - -// SetTheme sets the element's theme. -func (element *ScrollContainer) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.Propagator.SetTheme(new) - element.updateMinimumSize() - element.redoAll() -} - -// SetConfig sets the element's configuration. -func (element *ScrollContainer) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.Propagator.SetConfig(new) - element.updateMinimumSize() - element.redoAll() -} - -func (element *ScrollContainer) HandleScroll ( - x, y int, - deltaX, deltaY float64, -) { - horizontal, vertical := element.child.ScrollAxes() - if !horizontal { deltaX = 0 } - if !vertical { deltaY = 0 } - element.scrollChildBy(int(deltaX), int(deltaY)) -} - -// HandleKeyDown is called when a key is pressed down or repeated while -// this element has keyboard focus. It is important to note that not -// every key down event is guaranteed to be paired with exactly one key -// up event. This is the reason a list of modifier keys held down at the -// time of the key press is given. -func (element *ScrollContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - switch key { - case input.KeyPageUp: - viewport := element.child.ScrollViewportBounds() - element.HandleScroll(0, 0, 0, float64(-viewport.Dy())) - case input.KeyPageDown: - viewport := element.child.ScrollViewportBounds() - element.HandleScroll(0, 0, 0, float64(viewport.Dy())) - default: - element.Propagator.HandleKeyDown(key, modifiers) - } -} - -// HandleKeyUp is called when a key is released while this element has -// keyboard focus. -func (element *ScrollContainer) HandleKeyUp (key input.Key, modifiers input.Modifiers) { } - -// CountChildren returns the amount of children contained within this element. -func (element *ScrollContainer) CountChildren () (count int) { - return 3 -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *ScrollContainer) Child (index int) (child tomo.Element) { - switch index { - case 0: return element.child - case 1: - if element.horizontal == nil { - return nil - } else { - return element.horizontal - } - case 2: - if element.vertical == nil { - return nil - } else { - return element.vertical - } - default: return nil - } -} - -func (element *ScrollContainer) redoAll () { - if !element.core.HasImage() { return } - - zr := image.Rectangle { } - if element.child != nil { element.child.DrawTo(nil, zr, nil) } - if element.horizontal != nil { element.horizontal.DrawTo(nil, zr, nil) } - if element.vertical != nil { element.vertical.DrawTo(nil, zr, nil) } - - childBounds, horizontalBounds, verticalBounds := element.layout() - if element.child != nil { - element.child.DrawTo ( - canvas.Cut(element.core, childBounds), - childBounds, element.childDamageCallback) - } - if element.horizontal != nil { - element.horizontal.DrawTo ( - canvas.Cut(element.core, horizontalBounds), - horizontalBounds, element.childDamageCallback) - } - if element.vertical != nil { - element.vertical.DrawTo ( - canvas.Cut(element.core, verticalBounds), - verticalBounds, element.childDamageCallback) - } - element.draw() -} - -func (element *ScrollContainer) scrollChildBy (x, y int) { - if element.child == nil { return } - scrollPoint := - element.child.ScrollViewportBounds().Min. - Add(image.Pt(x, y)) - element.child.ScrollTo(scrollPoint) -} - -func (element *ScrollContainer) childDamageCallback (region image.Rectangle) { - element.core.DamageRegion(region) -} - -func (element *ScrollContainer) layout () ( - child image.Rectangle, - horizontal image.Rectangle, - vertical image.Rectangle, -) { - bounds := element.Bounds() - child = bounds - - if element.horizontal != nil { - _, hMinHeight := element.horizontal.MinimumSize() - child.Max.Y -= hMinHeight - } - if element.vertical != nil { - vMinWidth, _ := element.vertical.MinimumSize() - child.Max.X -= vMinWidth - } - - vertical.Min.X = child.Max.X - vertical.Max.X = bounds.Max.X - vertical.Min.Y = bounds.Min.Y - vertical.Max.Y = child.Max.Y - - horizontal.Min.X = bounds.Min.X - horizontal.Max.X = child.Max.X - horizontal.Min.Y = child.Max.Y - horizontal.Max.Y = bounds.Max.Y - return -} - -func (element *ScrollContainer) draw () { - if element.horizontal != nil && element.vertical != nil { - bounds := element.Bounds() - bounds.Min = image.Pt ( - bounds.Max.X - element.vertical.Bounds().Dx(), - bounds.Max.Y - element.horizontal.Bounds().Dy()) - state := tomo.State { } - deadArea := element.theme.Pattern(tomo.PatternDead, state) - deadArea.Draw(canvas.Cut(element.core, bounds), bounds) - } -} - -func (element *ScrollContainer) updateMinimumSize () { - var width, height int - - if element.child != nil { - width, height = element.child.MinimumSize() - } - if element.horizontal != nil { - hMinWidth, hMinHeight := element.horizontal.MinimumSize() - height += hMinHeight - if hMinWidth > width { - width = hMinWidth - } - } - if element.vertical != nil { - vMinWidth, vMinHeight := element.vertical.MinimumSize() - width += vMinWidth - if vMinHeight > height { - height = vMinHeight - } - } - element.core.SetMinimumSize(width, height) -} - -func (element *ScrollContainer) updateEnabled () { - horizontal, vertical := element.child.ScrollAxes() - if element.horizontal != nil { - element.horizontal.SetEnabled(horizontal) - } - if element.vertical != nil { - element.vertical.SetEnabled(vertical) - } -} diff --git a/elements/containers/table.go b/elements/containers/table.go deleted file mode 100644 index 4fe987f..0000000 --- a/elements/containers/table.go +++ /dev/null @@ -1,587 +0,0 @@ -package containers - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// TODO: using the event propagator core might not be the best idea here. we -// should have slightly different behavior to sync the focused element with the -// selected cell. alternatively we could pass a callback to the propagator that -// fires when the focused child changes. this would also allow things like -// scrolling to the focused child (for this element and others). - -type tableCell struct { - tomo.Element - tomo.Pattern - image.Rectangle -} - -// TableContainer is a container that lays its contents out in a table. It can -// be scrolled. -type TableContainer struct { - *core.Core - *core.Propagator - core core.CoreControl - - topHeading bool - leftHeading bool - - columns int - rows int - scroll image.Point - warping bool - grid [][]tableCell - children []tomo.Element - - contentBounds image.Rectangle - forcedMinimumWidth int - forcedMinimumHeight int - - selectedColumn int - selectedRow int - - config config.Wrapped - theme theme.Wrapped - - onSelect func () - onScrollBoundsChange func () -} - -// NewTable creates a new table element with the specified amount of columns and -// rows. If top or left heading is set to true, the first row or column -// respectively will display as a table header. -func NewTableContainer ( - columns, rows int, - topHeading, leftHeading bool, -) ( - element *TableContainer, -) { - element = &TableContainer { - topHeading: topHeading, - leftHeading: leftHeading, - selectedColumn: -1, - selectedRow: -1, - } - - element.theme.Case = tomo.C("tomo", "tableContainer") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - element.Resize(columns, rows) - return -} - -// Set places an element at the specified column and row. If the element passed -// is nil, whatever element occupies the cell currently is removed. -func (element *TableContainer) Set (column, row int, child tomo.Element) { - if row < 0 || row >= element.rows { return } - if column < 0 || column >= element.columns { return } - - childList := element.children - if child == nil { - if element.grid[row][column].Element == nil { - // no-op - return - } else { - // removing the child that is currently in a slow - element.unhook(element.grid[row][column].Element) - childList = childList[:len(childList) - 1] - element.grid[row][column].Element = child - } - } else { - element.hook(child) - if element.grid[row][column].Element == nil { - // putting the child in an empty slot - childList = append(childList, nil) - element.grid[row][column].Element = child - } else { - // replacing the child that is currently in a slow - element.unhook(element.grid[row][column].Element) - element.grid[row][column].Element = child - } - } - - element.rebuildChildList(childList) - element.children = childList - element.redoAll() -} - -// Resize changes the amount of columns and rows in the table. If the table is -// resized to be smaller, children in cells that do not exist anymore will be -// removed. The minimum size for a TableContainer is 1x1. -func (element *TableContainer) Resize (columns, rows int) { - if columns < 1 { columns = 1 } - if rows < 1 { rows = 1 } - if element.columns == columns && element.rows == rows { return } - amountRemoved := 0 - - // handle rows as a whole - if rows < element.rows { - // disown children in bottom rows - for _, row := range element.grid[rows:] { - for index, child := range row { - if child.Element != nil { - element.unhook(child.Element) - amountRemoved ++ - row[index].Element = nil - }}} - // cut grid to size - element.grid = element.grid[:rows] - } else { - // expand grid - newGrid := make([][]tableCell, rows) - copy(newGrid, element.grid) - element.grid = newGrid - } - - // handle each row individually - for rowIndex, row := range element.grid { - if columns < element.columns { - // disown children in the far right of the row - for index, child := range row[columns:] { - if child.Element != nil { - element.unhook(child.Element) - amountRemoved ++ - row[index].Element = nil - }} - // cut row to size - element.grid[rowIndex] = row[:columns] - } else { - // expand row - newRow := make([]tableCell, columns) - copy(newRow, row) - element.grid[rowIndex] = newRow - } - } - - element.columns = columns - element.rows = rows - - if amountRemoved > 0 { - childList := element.children[:len(element.children) - amountRemoved] - element.rebuildChildList(childList) - element.children = childList - } - element.redoAll() -} - -// Selected returns the column and row of the cell that is currently selected. -// If no cell is selected, this method will return (-1, -1). -func (element *TableContainer) Selected () (column, row int) { - return element.selectedColumn, element.selectedRow -} - -// OnSelect sets a function to be called when the user selects a table cell. -func (element *TableContainer) OnSelect (callback func ()) { - element.onSelect = callback -} - -// Warp runs the specified callback, deferring all layout and rendering updates -// until the callback has finished executing. This allows for aplications to -// perform batch gui updates without flickering and stuff. -func (element *TableContainer) Warp (callback func ()) { - if element.warping { - callback() - return - } - - element.warping = true - callback() - element.warping = false - - element.redoAll() -} - -// Collapse collapses the element's minimum width and height. A value of zero -// for either means that the element's normal value is used. -func (element *TableContainer) Collapse (width, height int) { - if - element.forcedMinimumWidth == width && - element.forcedMinimumHeight == height { - - return - } - - element.forcedMinimumWidth = width - element.forcedMinimumHeight = height - element.updateMinimumSize() -} - -// CountChildren returns the amount of children contained within this element. -func (element *TableContainer) CountChildren () (count int) { - return len(element.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *TableContainer) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index] -} - -func (element *TableContainer) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *TableContainer) NotifyMinimumSizeChange (child tomo.Element) { - element.updateMinimumSize() - element.redoAll() -} - -// DrawBackground draws a portion of the container's background pattern within -// the specified bounds. The container will not push these changes. -func (element *TableContainer) DrawBackground (bounds image.Rectangle) { - if !bounds.Overlaps(element.core.Bounds()) { return } - - for rowIndex, row := range element.grid { - for columnIndex, child := range row { - if bounds.Overlaps(child.Rectangle) { - element.theme.Pattern ( - child.Pattern, - element.state(columnIndex, rowIndex)). - Draw(canvas.Cut(element.core, bounds), child.Rectangle) - return - }}} -} - -func (element *TableContainer) HandleMouseDown (x, y int, button input.Button) { - element.Focus() - element.Propagator.HandleMouseDown(x, y, button) - if button != input.ButtonLeft { return } - - for rowIndex, row := range element.grid { - for columnIndex, child := range row { - if image.Pt(x, y).In(child.Rectangle) { - element.selectCell(columnIndex, rowIndex) - return - }}} -} - -func (element *TableContainer) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - switch key { - case input.KeyLeft: element.changeSelectionBy(-1, 0) - case input.KeyRight: element.changeSelectionBy(1, 0) - case input.KeyUp: element.changeSelectionBy(0, -1) - case input.KeyDown: element.changeSelectionBy(0, 1) - case input.KeyEscape: element.selectCell(-1, -1) - default: element.Propagator.HandleKeyDown(key, modifiers) - } -} - -// ScrollContentBounds returns the full content size of the element. -func (element *TableContainer) ScrollContentBounds () image.Rectangle { - return element.contentBounds -} - -// ScrollViewportBounds returns the size and position of the element's -// viewport relative to ScrollBounds. -func (element *TableContainer) ScrollViewportBounds () image.Rectangle { - bounds := element.Bounds() - bounds = bounds.Sub(bounds.Min).Add(element.scroll) - return bounds -} - -// ScrollTo scrolls the viewport to the specified point relative to -// ScrollBounds. -func (element *TableContainer) ScrollTo (position image.Point) { - if position.Y < 0 { - position.Y = 0 - } - maxScrollHeight := element.maxScrollHeight() - if position.Y > maxScrollHeight { - position.Y = maxScrollHeight - } - if position.X < 0 { - position.X = 0 - } - maxScrollWidth := element.maxScrollWidth() - if position.X > maxScrollWidth { - position.X = maxScrollWidth - } - element.scroll = position - if element.core.HasImage() && !element.warping { - element.redoAll() - element.core.DamageAll() - } -} - -// OnScrollBoundsChange sets a function to be called when the element's viewport -// bounds, content bounds, or scroll axes change. -func (element *TableContainer) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *TableContainer) ScrollAxes () (horizontal, vertical bool) { - return true, true -} - -func (element *TableContainer) changeSelectionBy (column, row int) { - column += element.selectedColumn - row += element.selectedRow - if column < 0 { column = 0 } - if row < 0 { row = 0 } - element.selectCell(column, row) -} - -func (element *TableContainer) selectCell (column, row int) { - if column < -1 { column = -1 } - if row < -1 { row = -1 } - if column >= element.columns { column = element.columns - 1 } - if row >= element.rows { row = element.rows - 1 } - - if column == element.selectedColumn && row == element.selectedRow { - return - } - - oldColumn, oldRow := element.selectedColumn, element.selectedRow - element.selectedColumn = column - element.selectedRow = row - if oldColumn >= 0 && oldRow >= 0 { - element.core.DamageRegion(element.redoCell(oldColumn, oldRow)) - } - if column >= 0 && row >= 0 { - element.core.DamageRegion(element.redoCell(column, row)) - } - if element.onSelect != nil { - element.onSelect() - } -} - -func (element *TableContainer) maxScrollHeight () (height int) { - viewportHeight := element.Bounds().Dy() - height = element.contentBounds.Dy() - viewportHeight - if height < 0 { height = 0 } - return -} - -func (element *TableContainer) maxScrollWidth () (width int) { - viewportWidth := element.Bounds().Dx() - width = element.contentBounds.Dx() - viewportWidth - if width < 0 { width = 0 } - return -} - -func (element *TableContainer) hook (child tomo.Element) { - if child0, ok := child.(tomo.Themeable); ok { - child0.SetTheme(element.theme.Theme) - } - if child0, ok := child.(tomo.Configurable); ok { - child0.SetConfig(element.config.Config) - } - child.SetParent(element) -} - -func (element *TableContainer) unhook (child tomo.Element) { - child.SetParent(nil) - child.DrawTo(nil, image.Rectangle { }, nil) -} - -func (element *TableContainer) rebuildChildList (list []tomo.Element) { - index := 0 - for _, row := range element.grid { - for _, child := range row { - if child.Element == nil { continue } - list[index] = child.Element - index ++ - }} -} - -func (element *TableContainer) state (column, row int) (state tomo.State) { - if column == element.selectedColumn && row == element.selectedRow { - state.On = true - } - return -} - -func (element *TableContainer) redoCell (column, row int) image.Rectangle { - padding := element.theme.Padding(tomo.PatternTableCell) - cell := element.grid[row][column] - pattern := element.theme.Pattern ( - cell.Pattern, element.state(column, row)) - - if cell.Element != nil { - // give child canvas portion - innerCellBounds := padding.Apply(cell.Rectangle) - artist.DrawShatter ( - element.core, pattern, - cell.Rectangle, innerCellBounds) - cell.DrawTo ( - canvas.Cut(element.core, innerCellBounds), - innerCellBounds, - element.childDrawCallback) - } else { - // draw cell pattern in empty cells - pattern.Draw(element.core, cell.Rectangle) - } - return cell.Rectangle -} - -func (element *TableContainer) redoAll () { - if element.warping || !element.core.HasImage() { - element.updateMinimumSize() - return - } - - maxScrollHeight := element.maxScrollHeight() - if element.scroll.Y > maxScrollHeight { - element.scroll.Y = maxScrollHeight - } - maxScrollWidth := element.maxScrollWidth() - if element.scroll.X > maxScrollWidth { - element.scroll.X = maxScrollWidth - } - - // calculate the minimum size of each column and row - var minWidth, minHeight float64 - columnWidths := make([]float64, element.columns) - rowHeights := make([]float64, element.rows) - padding := element.theme.Padding(tomo.PatternTableCell) - - for rowIndex, row := range element.grid { - for columnIndex, child := range row { - width, height := padding.Horizontal(), padding.Vertical() - - if child.Element != nil { - minWidth, minHeight := child.MinimumSize() - width += minWidth - height += minHeight - fwidth := float64(width) - fheight := float64(height) - if fwidth > columnWidths[columnIndex] { - columnWidths[columnIndex] = fwidth - } - if fheight > rowHeights[rowIndex] { - rowHeights[rowIndex] = fheight - } - } - }} - for _, width := range columnWidths { minWidth += width } - for _, height := range rowHeights { minHeight += height } - - // ignore given bounds for layout if they are below minimum size. we do - // this because we are scrollable in both directions and we might be - // collapsed. - bounds := element.Bounds().Sub(element.scroll) - if bounds.Dx() < int(minWidth) { - bounds.Max.X = bounds.Min.X + int(minWidth) - } - if bounds.Dy() < int(minHeight) { - bounds.Max.Y = bounds.Min.Y + int(minHeight) - } - element.contentBounds = bounds - - // scale up those minimum sizes to an actual size. - // FIXME: replace this with a more accurate algorithm - widthRatio := float64(bounds.Dx()) / minWidth - heightRatio := float64(bounds.Dy()) / minHeight - for index := range columnWidths { - columnWidths[index] *= widthRatio - } - for index := range rowHeights { - rowHeights[index] *= heightRatio - } - - // cut up canvas - x := float64(bounds.Min.X) - y := float64(bounds.Min.Y) - for rowIndex, row := range element.grid { - for columnIndex, _ := range row { - width := columnWidths[columnIndex] - height := rowHeights[rowIndex] - cellBounds := image.Rect ( - int(x), int(y), - int(x + width), int(y + height)) - - var id tomo.Pattern - isHeading := - rowIndex == 0 && element.topHeading || - columnIndex == 0 && element.leftHeading - if isHeading { - id = tomo.PatternTableHead - } else { - id = tomo.PatternTableCell - } - element.grid[rowIndex][columnIndex].Rectangle = cellBounds - element.grid[rowIndex][columnIndex].Pattern = id - - element.redoCell(columnIndex, rowIndex) - x += float64(width) - } - - x = float64(bounds.Min.X) - y += rowHeights[rowIndex] - } - - element.core.DamageAll() - - // update the minimum size of the element - if element.forcedMinimumHeight > 0 { - minHeight = float64(element.forcedMinimumHeight) - } - if element.forcedMinimumWidth > 0 { - minWidth = float64(element.forcedMinimumWidth) - } - element.core.SetMinimumSize(int(minWidth), int(minHeight)) - - // notify parent of scroll bounds change - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } -} - -func (element *TableContainer) updateMinimumSize () { - if element.forcedMinimumHeight > 0 && element.forcedMinimumWidth > 0 { - element.core.SetMinimumSize ( - element.forcedMinimumWidth, - element.forcedMinimumHeight) - return - } - - columnWidths := make([]int, element.columns) - rowHeights := make([]int, element.rows) - padding := element.theme.Padding(tomo.PatternTableCell) - - for rowIndex, row := range element.grid { - for columnIndex, child := range row { - width, height := padding.Horizontal(), padding.Vertical() - - if child.Element != nil { - minWidth, minHeight := child.MinimumSize() - width += minWidth - height += minHeight - if width > columnWidths[columnIndex] { - columnWidths[columnIndex] = width - } - if height > rowHeights[rowIndex] { - rowHeights[rowIndex] = height - } - } - }} - - var minWidth, minHeight int - for _, width := range columnWidths { minWidth += width } - for _, height := range rowHeights { minHeight += height } - - if element.forcedMinimumHeight > 0 { - minHeight = element.forcedMinimumHeight - } - if element.forcedMinimumWidth > 0 { - minWidth = element.forcedMinimumWidth - } - - element.core.SetMinimumSize(minWidth, minHeight) -} - -func (element *TableContainer) childDrawCallback (region image.Rectangle) { - element.core.DamageRegion(region) -} diff --git a/elements/core/core.go b/elements/core/core.go deleted file mode 100644 index 9811f14..0000000 --- a/elements/core/core.go +++ /dev/null @@ -1,235 +0,0 @@ -package core - -import "image" -import "image/color" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/shatter" - -// Core is a struct that implements some core functionality common to most -// widgets. It is meant to be embedded directly into a struct. -type Core struct { - canvas canvas.Canvas - bounds image.Rectangle - parent tomo.Parent - outer tomo.Element - - metrics struct { - minimumWidth int - minimumHeight int - } - - drawSizeChange func () - onDamage func (region image.Rectangle) -} - -// NewCore creates a new element core and its corresponding control given the -// element that it will be a part of. If outer is nil, this function will return -// nil. -func NewCore ( - outer tomo.Element, - drawSizeChange func (), -) ( - core *Core, - control CoreControl, -) { - if outer == nil { return } - core = &Core { - outer: outer, - drawSizeChange: drawSizeChange, - } - control = CoreControl { core: core } - return -} - -// Bounds fulfills the tomo.Element interface. This should not need to be -// overridden. -func (core *Core) Bounds () (bounds image.Rectangle) { - if core.canvas == nil { return } - return core.bounds -} - -// MinimumSize fulfils the tomo.Element interface. This should not need to be -// overridden. -func (core *Core) MinimumSize () (width, height int) { - return core.metrics.minimumWidth, core.metrics.minimumHeight -} - -// MinimumSize fulfils the tomo.Element interface. This should not need to be -// overridden, unless you want to detect when the element is parented or -// unparented. -func (core *Core) SetParent (parent tomo.Parent) { - if parent != nil && core.parent != nil { - panic("core.SetParent: element already has a parent") - } - - core.parent = parent -} - -// DrawTo fulfills the tomo.Element interface. This should not need to be -// overridden. -func (core *Core) DrawTo ( - canvas canvas.Canvas, - bounds image.Rectangle, - onDamage func (region image.Rectangle), -) { - core.canvas = canvas - core.bounds = bounds - core.onDamage = onDamage - if core.drawSizeChange != nil && core.canvas != nil { - core.drawSizeChange() - } -} - -// CoreControl is a struct that can exert control over a Core struct. It can be -// used as a canvas. It must not be directly embedded into an element, but -// instead kept as a private member. When a Core struct is created, a -// corresponding CoreControl struct is linked to it and returned alongside it. -type CoreControl struct { - core *Core -} - -// ColorModel fulfills the draw.Image interface. -func (control CoreControl) ColorModel () (model color.Model) { - return color.RGBAModel -} - -// At fulfills the draw.Image interface. -func (control CoreControl) At (x, y int) (pixel color.Color) { - if control.core.canvas == nil { return } - return control.core.canvas.At(x, y) -} - -// Bounds fulfills the draw.Image interface. -func (control CoreControl) Bounds () (bounds image.Rectangle) { - if control.core.canvas == nil { return } - return control.core.canvas.Bounds() -} - -// Set fulfills the draw.Image interface. -func (control CoreControl) Set (x, y int, c color.Color) () { - if control.core.canvas == nil { return } - control.core.canvas.Set(x, y, c) -} - -// Buffer fulfills the canvas.Canvas interface. -func (control CoreControl) Buffer () (data []color.RGBA, stride int) { - if control.core.canvas == nil { return } - return control.core.canvas.Buffer() -} - -// Parent returns the element's parent. -func (control CoreControl) Parent () tomo.Parent { - return control.core.parent -} - -// DrawBackground fills the element's canvas with the parent's background -// pattern, if the parent supports it. If it is not supported, the fallback -// pattern will be used instead. -func (control CoreControl) DrawBackground (fallback artist.Pattern) { - control.DrawBackgroundBounds(fallback, control.Bounds()) -} - -// DrawBackgroundBounds is like DrawBackground, but it takes in a bounding -// rectangle instead of using the element's bounds. -func (control CoreControl) DrawBackgroundBounds ( - fallback artist.Pattern, - bounds image.Rectangle, -) { - parent, ok := control.Parent().(tomo.BackgroundParent) - if ok { - parent.DrawBackground(bounds) - } else if fallback != nil { - fallback.Draw(canvas.Cut(control, bounds), control.Bounds()) - } -} - -// DrawBackgroundBoundsShatter is like DrawBackgroundBounds, but uses the -// shattering algorithm to avoid drawing in areas specified by rocks. -func (control CoreControl) DrawBackgroundBoundsShatter ( - fallback artist.Pattern, - bounds image.Rectangle, - rocks ...image.Rectangle, -) { - tiles := shatter.Shatter(bounds, rocks...) - for _, tile := range tiles { - control.DrawBackgroundBounds(fallback, tile) - } -} - -// Window returns the window containing the element. -func (control CoreControl) Window () tomo.Window { - parent := control.Parent() - if parent == nil { - return nil - } else { - return parent.Window() - } -} - -// Outer returns the outer element given when the control was constructed. -func (control CoreControl) Outer () tomo.Element { - return control.core.outer -} - -// HasImage returns true if the core has an allocated image buffer, and false if -// it doesn't. -func (control CoreControl) HasImage () (has bool) { - return control.core.canvas != nil && !control.core.canvas.Bounds().Empty() -} - -// DamageRegion pushes the selected region of pixels to the parent element. This -// does not need to be called when responding to a resize event. -func (control CoreControl) DamageRegion (regions ...image.Rectangle) { - if control.core.canvas == nil { return } - if control.core.onDamage != nil { - for _, region := range regions { - control.core.onDamage(region) - } - } -} - -// DamageAll pushes all pixels to the parent element. This does not need to be -// called when redrawing in response to a change in size. -func (control CoreControl) DamageAll () { - control.DamageRegion(control.core.Bounds()) -} - -// SetMinimumSize sets the minimum size of this element, notifying the parent -// element in the process. -func (control CoreControl) SetMinimumSize (width, height int) { - core := control.core - if width == core.metrics.minimumWidth && - height == core.metrics.minimumHeight { - return - } - - core.metrics.minimumWidth = width - core.metrics.minimumHeight = height - if control.core.parent != nil { - control.core.parent.NotifyMinimumSizeChange(control.core.outer) - } -} - -// ConstrainSize contstrains the specified width and height to the minimum width -// and height, and returns wether or not anything ended up being constrained. -func (control CoreControl) ConstrainSize ( - inWidth, inHeight int, -) ( - outWidth, outHeight int, - constrained bool, -) { - core := control.core - outWidth = inWidth - outHeight = inHeight - if outWidth < core.metrics.minimumWidth { - outWidth = core.metrics.minimumWidth - constrained = true - } - if outHeight < core.metrics.minimumHeight { - outHeight = core.metrics.minimumHeight - constrained = true - } - return -} diff --git a/elements/core/doc.go b/elements/core/doc.go deleted file mode 100644 index 5870f79..0000000 --- a/elements/core/doc.go +++ /dev/null @@ -1,7 +0,0 @@ -// Package core provides tools that allow elements to easily fulfill common -// interfaces without having to duplicate a ton of code. Each "core" is a type -// that can be embedded into an element directly, working to fulfill a -// particular interface. Each one comes with a corresponding core control, which -// provides an interface for elements to exert control over the core. Core -// controls should be kept private. -package core diff --git a/elements/core/focusable.go b/elements/core/focusable.go deleted file mode 100644 index ed3627f..0000000 --- a/elements/core/focusable.go +++ /dev/null @@ -1,101 +0,0 @@ -package core - -// import "runtime/debug" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" - -// FocusableCore is a struct that can be embedded into objects to make them -// focusable, giving them the default keynav behavior. -type FocusableCore struct { - core CoreControl - focused bool - enabled bool - drawFocusChange func () -} - -// NewFocusableCore creates a new focusability core and its corresponding -// control. If your element needs to visually update itself when it's focus -// state changes (which it should), a callback to draw and push the update can -// be specified. -func NewFocusableCore ( - core CoreControl, - drawFocusChange func (), -) ( - focusable *FocusableCore, - control FocusableCoreControl, -) { - focusable = &FocusableCore { - core: core, - drawFocusChange: drawFocusChange, - enabled: true, - } - control = FocusableCoreControl { core: focusable } - return -} - -// Focused returns whether or not this element is currently focused. -func (core *FocusableCore) Focused () (focused bool) { - return core.focused -} - -// Focus focuses this element, if its parent element grants the request. -func (core *FocusableCore) Focus () { - if !core.enabled || core.focused { return } - parent := core.core.Parent() - if parent, ok := parent.(tomo.FocusableParent); ok { - core.focused = parent.RequestFocus ( - core.core.Outer().(tomo.Focusable)) - } -} - -// HandleFocus causes this element to mark itself as focused, if it can -// currently be. Otherwise, it will return false and do nothing. -func (core *FocusableCore) HandleFocus ( - direction input.KeynavDirection, -) ( - accepted bool, -) { - direction = direction.Canon() - if !core.enabled { return false } - if core.focused && direction != input.KeynavDirectionNeutral { - return false - } - - if core.focused == false { - core.focused = true - if core.drawFocusChange != nil { core.drawFocusChange() } - } - return true -} - -// HandleUnfocus causes this element to mark itself as unfocused. -func (core *FocusableCore) HandleUnfocus () { - core.focused = false - // debug.PrintStack() - if core.drawFocusChange != nil { core.drawFocusChange() } -} - -// Enabled returns whether or not the element is enabled. -func (core *FocusableCore) Enabled () (enabled bool) { - return core.enabled -} - -// FocusableCoreControl is a struct that can be used to exert control over a -// focusability core. It must not be directly embedded into an element, but -// instead kept as a private member. When a FocusableCore struct is created, a -// corresponding FocusableCoreControl struct is linked to it and returned -// alongside it. -type FocusableCoreControl struct { - core *FocusableCore -} - -// SetEnabled sets whether the focusability core is enabled. If the state -// changes, this will call drawFocusChange. -func (control FocusableCoreControl) SetEnabled (enabled bool) { - if control.core.enabled == enabled { return } - control.core.enabled = enabled - if !enabled { control.core.focused = false } - if control.core.drawFocusChange != nil { - control.core.drawFocusChange() - } -} diff --git a/elements/core/propagator.go b/elements/core/propagator.go deleted file mode 100644 index 656fce1..0000000 --- a/elements/core/propagator.go +++ /dev/null @@ -1,355 +0,0 @@ -package core - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" - -// Container represents an object that can provide access to a list of child -// elements. -type Container interface { - Child (index int) tomo.Element - CountChildren () int -} - -// Propagator is a struct that can be embedded into elements that contain one or -// more children in order to propagate events to them without having to write -// all of the event handlers. It also implements standard behavior for focus -// propagation and keyboard navigation. -type Propagator struct { - core CoreControl - container Container - drags [10]tomo.MouseTarget - focused bool -} - -// NewPropagator creates a new event propagator that uses the specified -// container to access a list of child elements that will have events propagated -// to them. If container is nil, the function will return nil. -func NewPropagator (container Container, core CoreControl) (propagator *Propagator) { - if container == nil { return nil } - propagator = &Propagator { - core: core, - container: container, - } - return -} - -// ----------- Interface fulfillment methods ----------- // - -// Focused returns whether or not this element or any of its children -// are currently focused. -func (propagator *Propagator) Focused () (focused bool) { - return propagator.focused -} - -// Focus focuses this element, if its parent element grants the -// request. -func (propagator *Propagator) Focus () { - if propagator.focused == true { return } - parent := propagator.core.Parent() - if parent, ok := parent.(tomo.FocusableParent); ok && parent != nil { - propagator.focused = parent.RequestFocus ( - propagator.core.Outer().(tomo.Focusable)) - } -} - -// HandleFocus causes this element to mark itself as focused. If the -// element does not have children or there are no more focusable children in -// the given direction, it should return false and do nothing. Otherwise, it -// marks itself as focused along with any applicable children and returns -// true. -func (propagator *Propagator) HandleFocus (direction input.KeynavDirection) (accepted bool) { - direction = direction.Canon() - - firstFocused := propagator.firstFocused() - if firstFocused < 0 { - // no element is currently focused, so we need to focus either - // the first or last focusable element depending on the - // direction. - switch direction { - case input.KeynavDirectionForward: - // if we recieve a forward direction, focus the first - // focusable element. - return propagator.focusFirstFocusableElement(direction) - - case input.KeynavDirectionBackward: - // if we recieve a backward direction, focus the last - // focusable element. - return propagator.focusLastFocusableElement(direction) - - case input.KeynavDirectionNeutral: - // if we recieve a neutral direction, just focus this - // element and nothing else. - propagator.focused = true - return true - } - } else { - // an element is currently focused, so we need to move the - // focus in the specified direction - firstFocusedChild := - propagator.container.Child(firstFocused). - (tomo.Focusable) - - // before we move the focus, the currently focused child - // may also be able to move its focus. if the child is able - // to do that, we will let it and not move ours. - if firstFocusedChild.HandleFocus(direction) { - return true - } - - // find the previous/next focusable element relative to the - // currently focused element, if it exists. - for index := firstFocused + int(direction); - index < propagator.container.CountChildren() && index >= 0; - index += int(direction) { - - child, focusable := - propagator.container.Child(index). - (tomo.Focusable) - if focusable && child.HandleFocus(direction) { - // we have found one, so we now actually move - // the focus. - firstFocusedChild.HandleUnfocus() - propagator.focused = true - return true - } - } - } - - return false -} - -// RequestFocus notifies the parent that a child element is requesting -// keyboard focus. If the parent grants the request, the method will -// return true and the child element should behave as if a HandleFocus -// call was made. -func (propagator *Propagator) RequestFocus ( - child tomo.Focusable, -) ( - granted bool, -) { - if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok { - if parent.RequestFocus(propagator.core.Outer().(tomo.Focusable)) { - propagator.HandleUnfocus() - propagator.focused = true - granted = true - } - } - return -} - -// RequestFocusMotion notifies the parent that a child element wants the -// focus to be moved to the next focusable element. -func (propagator *Propagator) RequestFocusNext (child tomo.Focusable) { - if !propagator.focused { return } - if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok { - parent.RequestFocusNext(propagator.core.Outer().(tomo.Focusable)) - } -} - -// RequestFocusMotion notifies the parent that a child element wants the -// focus to be moved to the previous focusable element. -func (propagator *Propagator) RequestFocusPrevious (child tomo.Focusable) { - if !propagator.focused { return } - if parent, ok := propagator.core.Parent().(tomo.FocusableParent); ok { - parent.RequestFocusPrevious(propagator.core.Outer().(tomo.Focusable)) - } -} - -// HandleDeselection causes this element to mark itself and all of its children -// as unfocused. -func (propagator *Propagator) HandleUnfocus () { - propagator.forFocusable (func (child tomo.Focusable) bool { - child.HandleUnfocus() - return true - }) - propagator.focused = false -} - -// HandleKeyDown propogates the keyboard event to the currently selected child. -func (propagator *Propagator) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - propagator.forFocused (func (child tomo.Focusable) bool { - typedChild, handlesKeyboard := child.(tomo.KeyboardTarget) - if handlesKeyboard { - typedChild.HandleKeyDown(key, modifiers) - } - return true - }) -} - -// HandleKeyUp propogates the keyboard event to the currently selected child. -func (propagator *Propagator) HandleKeyUp (key input.Key, modifiers input.Modifiers) { - propagator.forFocused (func (child tomo.Focusable) bool { - typedChild, handlesKeyboard := child.(tomo.KeyboardTarget) - if handlesKeyboard { - typedChild.HandleKeyUp(key, modifiers) - } - return true - }) -} - -// HandleMouseDown propagates the mouse event to the element under the mouse -// pointer. -func (propagator *Propagator) HandleMouseDown (x, y int, button input.Button) { - child, handlesMouse := - propagator.childAt(image.Pt(x, y)). - (tomo.MouseTarget) - if handlesMouse { - propagator.drags[button] = child - child.HandleMouseDown(x, y, button) - } -} - -// HandleMouseUp propagates the mouse event to the element that the released -// mouse button was originally pressed on. -func (propagator *Propagator) HandleMouseUp (x, y int, button input.Button) { - child := propagator.drags[button] - if child != nil { - propagator.drags[button] = nil - child.HandleMouseUp(x, y, button) - } -} - -// HandleMotion propagates the mouse event to the element that was last -// pressed down by the mouse if the mouse is currently being held down, else it -// propagates the event to whichever element is underneath the mouse pointer. -func (propagator *Propagator) HandleMotion (x, y int) { - handled := false - for _, child := range propagator.drags { - if child, ok := child.(tomo.MotionTarget); ok { - child.HandleMotion(x, y) - handled = true - } - } - - if !handled { - child := propagator.childAt(image.Pt(x, y)) - if child, ok := child.(tomo.MotionTarget); ok { - child.HandleMotion(x, y) - } - } -} - -// HandleScroll propagates the mouse event to the element under the mouse -// pointer. -func (propagator *Propagator) HandleScroll (x, y int, deltaX, deltaY float64) { - child := propagator.childAt(image.Pt(x, y)) - if child, ok := child.(tomo.ScrollTarget); ok { - child.HandleScroll(x, y, deltaX, deltaY) - } -} - -// SetTheme sets the theme of all children to the specified theme. -func (propagator *Propagator) SetTheme (theme tomo.Theme) { - propagator.forChildren (func (child tomo.Element) bool { - typedChild, themeable := child.(tomo.Themeable) - if themeable { - typedChild.SetTheme(theme) - } - return true - }) -} - -// SetConfig sets the theme of all children to the specified config. -func (propagator *Propagator) SetConfig (config tomo.Config) { - propagator.forChildren (func (child tomo.Element) bool { - typedChild, configurable := child.(tomo.Configurable) - if configurable { - typedChild.SetConfig(config) - } - return true - }) -} - -// ----------- Focusing utilities ----------- // - -func (propagator *Propagator) focusFirstFocusableElement ( - direction input.KeynavDirection, -) ( - ok bool, -) { - propagator.forFocusable (func (child tomo.Focusable) bool { - if child.HandleFocus(direction) { - propagator.focused = true - ok = true - return false - } - return true - }) - return -} - -func (propagator *Propagator) focusLastFocusableElement ( - direction input.KeynavDirection, -) ( - ok bool, -) { - propagator.forChildrenReverse (func (child tomo.Element) bool { - typedChild, focusable := child.(tomo.Focusable) - if focusable && typedChild.HandleFocus(direction) { - propagator.focused = true - ok = true - return false - } - return true - }) - return -} - -// ----------- Iterator utilities ----------- // - -func (propagator *Propagator) forChildren (callback func (child tomo.Element) bool) { - for index := 0; index < propagator.container.CountChildren(); index ++ { - child := propagator.container.Child(index) - if child == nil { continue } - if !callback(child) { break } - } -} - -func (propagator *Propagator) forChildrenReverse (callback func (child tomo.Element) bool) { - for index := propagator.container.CountChildren() - 1; index > 0; index -- { - child := propagator.container.Child(index) - if child == nil { continue } - if !callback(child) { break } - } -} - -func (propagator *Propagator) childAt (position image.Point) (child tomo.Element) { - propagator.forChildren (func (current tomo.Element) bool { - if position.In(current.Bounds()) { - child = current - } - return true - }) - return -} - -func (propagator *Propagator) forFocused (callback func (child tomo.Focusable) bool) { - propagator.forChildren (func (child tomo.Element) bool { - typedChild, focusable := child.(tomo.Focusable) - if focusable && typedChild.Focused() { - if !callback(typedChild) { return false } - } - return true - }) -} - -func (propagator *Propagator) forFocusable (callback func (child tomo.Focusable) bool) { - propagator.forChildren (func (child tomo.Element) bool { - typedChild, focusable := child.(tomo.Focusable) - if focusable { - if !callback(typedChild) { return false } - } - return true - }) -} - -func (propagator *Propagator) firstFocused () int { - for index := 0; index < propagator.container.CountChildren(); index ++ { - child, focusable := propagator.container.Child(index).(tomo.Focusable) - if focusable && child.Focused() { - return index - } - } - return -1 -} diff --git a/elements/directory.go b/elements/directory.go new file mode 100644 index 0000000..6859d59 --- /dev/null +++ b/elements/directory.go @@ -0,0 +1,312 @@ +package elements + +import "image" +import "path/filepath" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +// TODO: base on flow implementation of list. also be able to switch to a table +// variant for a more information dense view. + +type directoryEntity interface { + tomo.ContainerEntity + tomo.ScrollableEntity +} + +type historyEntry struct { + location string + filesystem ReadDirStatFS +} + +// Directory displays a list of files within a particular directory and +// file system. +type Directory struct { + container + entity directoryEntity + theme theme.Wrapped + + scroll image.Point + contentBounds image.Rectangle + + history []historyEntry + historyIndex int + + onChoose func (file string) + onScrollBoundsChange func () +} + +// NewDirectory creates a new directory view. If within is nil, it will use +// the OS file system. +func NewDirectory ( + location string, + within ReadDirStatFS, +) ( + element *Directory, + err error, +) { + element = &Directory { } + element.theme.Case = tomo.C("tomo", "list") + element.entity = tomo.NewEntity(element).(directoryEntity) + element.container.entity = element.entity + element.minimumSize = element.updateMinimumSize + element.init() + err = element.SetLocation(location, within) + return +} + +func (element *Directory) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() + } + + tiles := shatter.Shatter(element.entity.Bounds(), rocks...) + for _, tile := range tiles { + element.DrawBackground(canvas.Cut(destination, tile)) + } +} + +func (element *Directory) Layout () { + if element.scroll.Y > element.maxScrollHeight() { + element.scroll.Y = element.maxScrollHeight() + } + + margin := element.theme.Margin(tomo.PatternPinboard) + padding := element.theme.Padding(tomo.PatternPinboard) + bounds := padding.Apply(element.entity.Bounds()) + element.contentBounds = image.Rectangle { } + + dot := bounds.Min.Sub(element.scroll) + xStart := dot.X + rowHeight := 0 + + nextLine := func () { + dot.X = xStart + dot.Y += margin.Y + dot.Y += rowHeight + rowHeight = 0 + } + + for index := 0; index < element.entity.CountChildren(); index ++ { + child := element.entity.Child(index) + entry := element.scratch[child] + + width := int(entry.minBreadth) + height := int(entry.minSize) + if width + dot.X > bounds.Max.X { + nextLine() + } + if typedChild, ok := child.(tomo.Flexible); ok { + height = typedChild.FlexibleHeightFor(width) + } + if rowHeight < height { + rowHeight = height + } + + childBounds := tomo.Bounds ( + dot.X, dot.Y, + width, height) + element.entity.PlaceChild(index, childBounds) + element.contentBounds = element.contentBounds.Union(childBounds) + + dot.X += width + margin.X + } + + element.contentBounds = + element.contentBounds.Sub(element.contentBounds.Min) + + element.entity.NotifyScrollBoundsChange() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *Directory) HandleMouseDown (x, y int, button input.Button) { + element.selectNone() +} + +func (element *Directory) HandleMouseUp (x, y int, button input.Button) { } + +func (element *Directory) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) { + element.selectNone() + if child, ok := child.(tomo.Selectable); ok { + index := element.entity.IndexOf(child) + element.entity.SelectChild(index, true) + } +} + +func (element *Directory) HandleChildMouseUp (int, int, input.Button, tomo.Element) { } + +func (element *Directory) HandleChildFlexibleHeightChange (child tomo.Flexible) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// ScrollContentBounds returns the full content size of the element. +func (element *Directory) ScrollContentBounds () image.Rectangle { + return element.contentBounds +} + +// ScrollViewportBounds returns the size and position of the element's +// viewport relative to ScrollBounds. +func (element *Directory) ScrollViewportBounds () image.Rectangle { + padding := element.theme.Padding(tomo.PatternPinboard) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min).Add(element.scroll) + return bounds +} + +// ScrollTo scrolls the viewport to the specified point relative to +// ScrollBounds. +func (element *Directory) ScrollTo (position image.Point) { + if position.Y < 0 { + position.Y = 0 + } + maxScrollHeight := element.maxScrollHeight() + if position.Y > maxScrollHeight { + position.Y = maxScrollHeight + } + element.scroll = position + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *Directory) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + +// ScrollAxes returns the supported axes for scrolling. +func (element *Directory) ScrollAxes () (horizontal, vertical bool) { + return false, true +} + +func (element *Directory) DrawBackground (destination canvas.Canvas) { + element.theme.Pattern(tomo.PatternPinboard, tomo.State { }). + Draw(destination, element.entity.Bounds()) +} + +// SetTheme sets the element's theme. +func (element *Directory) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// Location returns the directory's location and filesystem. +func (element *Directory) Location () (string, ReadDirStatFS) { + if len(element.history) < 1 { return "", nil } + current := element.history[element.historyIndex] + return current.location, current.filesystem +} + +// SetLocation sets the directory's location and filesystem. If within is nil, +// it will use the OS file system. +func (element *Directory) SetLocation ( + location string, + within ReadDirStatFS, +) error { + if within == nil { + within = defaultFS { } + } + element.scroll = image.Point { } + + if element.history != nil { + element.historyIndex ++ + } + element.history = append ( + element.history[:element.historyIndex], + historyEntry { location, within }) + return element.Update() +} + +// Backward goes back a directory in history +func (element *Directory) Backward () (bool, error) { + if element.historyIndex > 1 { + element.historyIndex -- + return true, element.Update() + } else { + return false, nil + } +} + +// Forward goes forward a directory in history +func (element *Directory) Forward () (bool, error) { + if element.historyIndex < len(element.history) - 1 { + element.historyIndex ++ + return true, element.Update() + } else { + return false, nil + } +} + +// Update refreshes the directory's contents. +func (element *Directory) Update () error { + location, filesystem := element.Location() + entries, err := filesystem.ReadDir(location) + + children := make([]tomo.Element, len(entries)) + for index, entry := range entries { + filePath := filepath.Join(location, entry.Name()) + file, _ := NewFile(filePath, filesystem) + file.OnChoose (func () { + if element.onChoose != nil { + element.onChoose(filePath) + } + }) + + children[index] = file + } + + element.DisownAll() + element.Adopt(children...) + return err +} + +// OnChoose sets a function to be called when the user double-clicks a file or +// sub-directory within the directory view. +func (element *Directory) OnChoose (callback func (file string)) { + element.onChoose = callback +} + +func (element *Directory) selectNone () { + for index := 0; index < element.entity.CountChildren(); index ++ { + element.entity.SelectChild(index, false) + } +} + +func (element *Directory) maxScrollHeight () (height int) { + padding := element.theme.Padding(tomo.PatternSunken) + viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() + height = element.contentBounds.Dy() - viewportHeight + if height < 0 { height = 0 } + return +} + + +func (element *Directory) updateMinimumSize () { + padding := element.theme.Padding(tomo.PatternPinboard) + minimumWidth := 0 + for index := 0; index < element.entity.CountChildren(); index ++ { + width, height := element.entity.ChildMinimumSize(index) + if width > minimumWidth { + minimumWidth = width + } + + key := element.entity.Child(index) + entry := element.scratch[key] + entry.minSize = float64(height) + entry.minBreadth = float64(width) + element.scratch[key] = entry + } + element.entity.SetMinimumSize ( + minimumWidth + padding.Horizontal(), + padding.Vertical()) +} diff --git a/elements/document.go b/elements/document.go new file mode 100644 index 0000000..9954322 --- /dev/null +++ b/elements/document.go @@ -0,0 +1,208 @@ +package elements + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/shatter" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" + +type documentEntity interface { + tomo.ContainerEntity + tomo.ScrollableEntity +} + +type Document struct { + container + entity documentEntity + + scroll image.Point + contentBounds image.Rectangle + + theme theme.Wrapped + + onScrollBoundsChange func () +} + +func NewDocument (children ...tomo.Element) (element *Document) { + element = &Document { } + element.theme.Case = tomo.C("tomo", "document") + element.entity = tomo.NewEntity(element).(documentEntity) + element.container.entity = element.entity + element.minimumSize = element.updateMinimumSize + element.init() + element.Adopt(children...) + return +} + +func (element *Document) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() + } + + tiles := shatter.Shatter(element.entity.Bounds(), rocks...) + for _, tile := range tiles { + element.entity.DrawBackground(canvas.Cut(destination, tile)) + } +} + +func (element *Document) Layout () { + if element.scroll.Y > element.maxScrollHeight() { + element.scroll.Y = element.maxScrollHeight() + } + + margin := element.theme.Margin(tomo.PatternBackground) + padding := element.theme.Padding(tomo.PatternBackground) + bounds := padding.Apply(element.entity.Bounds()) + element.contentBounds = image.Rectangle { } + + dot := bounds.Min.Sub(element.scroll) + xStart := dot.X + rowHeight := 0 + + nextLine := func () { + dot.X = xStart + dot.Y += margin.Y + dot.Y += rowHeight + rowHeight = 0 + } + + for index := 0; index < element.entity.CountChildren(); index ++ { + child := element.entity.Child(index) + entry := element.scratch[child] + + if dot.X > xStart && entry.expand { + nextLine() + } + + width := int(entry.minBreadth) + height := int(entry.minSize) + if width + dot.X > bounds.Max.X && !entry.expand { + nextLine() + } + if width < bounds.Dx() && entry.expand { + width = bounds.Dx() + } + if typedChild, ok := child.(tomo.Flexible); ok { + height = typedChild.FlexibleHeightFor(width) + } + if rowHeight < height { + rowHeight = height + } + + childBounds := tomo.Bounds ( + dot.X, dot.Y, + width, height) + element.entity.PlaceChild(index, childBounds) + element.contentBounds = element.contentBounds.Union(childBounds) + + if entry.expand { + nextLine() + } else { + dot.X += width + margin.X + } + } + + element.contentBounds = + element.contentBounds.Sub(element.contentBounds.Min) + + element.entity.NotifyScrollBoundsChange() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *Document) Adopt (children ...tomo.Element) { + element.adopt(true, children...) +} + +func (element *Document) AdoptInline (children ...tomo.Element) { + element.adopt(false, children...) +} + +func (element *Document) HandleChildFlexibleHeightChange (child tomo.Flexible) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Document) DrawBackground (destination canvas.Canvas) { + element.entity.DrawBackground(destination) +} + +// SetTheme sets the element's theme. +func (element *Document) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// ScrollContentBounds returns the full content size of the element. +func (element *Document) ScrollContentBounds () image.Rectangle { + return element.contentBounds +} + +// ScrollViewportBounds returns the size and position of the element's +// viewport relative to ScrollBounds. +func (element *Document) ScrollViewportBounds () image.Rectangle { + padding := element.theme.Padding(tomo.PatternBackground) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min).Add(element.scroll) + return bounds +} + +// ScrollTo scrolls the viewport to the specified point relative to +// ScrollBounds. +func (element *Document) ScrollTo (position image.Point) { + if position.Y < 0 { + position.Y = 0 + } + maxScrollHeight := element.maxScrollHeight() + if position.Y > maxScrollHeight { + position.Y = maxScrollHeight + } + element.scroll = position + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +// OnScrollBoundsChange sets a function to be called when the element's viewport +// bounds, content bounds, or scroll axes change. +func (element *Document) OnScrollBoundsChange (callback func ()) { + element.onScrollBoundsChange = callback +} + +// ScrollAxes returns the supported axes for scrolling. +func (element *Document) ScrollAxes () (horizontal, vertical bool) { + return false, true +} + +func (element *Document) maxScrollHeight () (height int) { + padding := element.theme.Padding(tomo.PatternSunken) + viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() + height = element.contentBounds.Dy() - viewportHeight + if height < 0 { height = 0 } + return +} + +func (element *Document) updateMinimumSize () { + padding := element.theme.Padding(tomo.PatternBackground) + minimumWidth := 0 + for index := 0; index < element.entity.CountChildren(); index ++ { + width, height := element.entity.ChildMinimumSize(index) + if width > minimumWidth { + minimumWidth = width + } + + key := element.entity.Child(index) + entry := element.scratch[key] + entry.minSize = float64(height) + entry.minBreadth = float64(width) + element.scratch[key] = entry + } + element.entity.SetMinimumSize ( + minimumWidth + padding.Horizontal(), + padding.Vertical()) +} diff --git a/elements/file/file.go b/elements/file.go similarity index 66% rename from elements/file/file.go rename to elements/file.go index 62e6e4d..6d5835c 100644 --- a/elements/file/file.go +++ b/elements/file.go @@ -1,4 +1,4 @@ -package fileElements +package elements import "time" import "io/fs" @@ -6,27 +6,29 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +type fileEntity interface { + tomo.SelectableEntity + tomo.FocusableEntity +} + // File displays an interactive visual representation of a file within any // file system. type File struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity fileEntity config config.Wrapped theme theme.Wrapped lastClick time.Time pressed bool + enabled bool iconID tomo.Icon filesystem fs.StatFS location string - selected bool onChoose func () } @@ -40,15 +42,44 @@ func NewFile ( element *File, err error, ) { - element = &File { } + element = &File { enabled: true } element.theme.Case = tomo.C("files", "file") - element.Core, element.core = core.NewCore(element, element.drawAll) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.drawAndPush) + element.entity = tomo.NewEntity(element).(fileEntity) err = element.SetLocation(location, within) return } +// Entity returns this element's entity. +func (element *File) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *File) Draw (destination canvas.Canvas) { + // background + state := element.state() + bounds := element.entity.Bounds() + sink := element.theme.Sink(tomo.PatternButton) + element.theme. + Pattern(tomo.PatternButton, state). + Draw(destination, bounds) + + // icon + icon := element.icon() + if icon != nil { + iconBounds := icon.Bounds() + offset := image.Pt ( + (bounds.Dx() - iconBounds.Dx()) / 2, + (bounds.Dy() - iconBounds.Dy()) / 2) + if element.pressed { + offset = offset.Add(sink) + } + icon.Draw ( + destination, + element.theme.Color(tomo.ColorForeground, state), + bounds.Min.Add(offset)) + } +} // Location returns the file's location and filesystem. func (element *File) Location () (string, fs.StatFS) { return element.location, element.filesystem @@ -82,55 +113,70 @@ func (element *File) Update () error { } element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() return err } -func (element *File) Selected () bool { - return element.selected -} - -func (element *File) SetSelected (selected bool) { - if element.selected == selected { return } - element.selected = selected - element.drawAndPush() -} - func (element *File) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if !element.Enabled() { return } if key == input.KeyEnter { element.pressed = true - element.drawAndPush() + element.entity.Invalidate() } } func (element *File) HandleKeyUp(key input.Key, modifiers input.Modifiers) { if key == input.KeyEnter && element.pressed { element.pressed = false - element.drawAndPush() if !element.Enabled() { return } + element.entity.Invalidate() if element.onChoose != nil { element.onChoose() } } } +func (element *File) HandleFocusChange () { + element.entity.Invalidate() +} + +func (element *File) HandleSelectionChange () { + element.entity.Invalidate() +} + func (element *File) OnChoose (callback func ()) { element.onChoose = callback } +// Focus gives this element input focus. +func (element *File) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this file is enabled or not. +func (element *File) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this file is enabled or not. +func (element *File) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + func (element *File) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } - if !element.Focused() { element.Focus() } + if !element.entity.Focused() { element.Focus() } if button != input.ButtonLeft { return } element.pressed = true - element.drawAndPush() + element.entity.Invalidate() } func (element *File) HandleMouseUp (x, y int, button input.Button) { if button != input.ButtonLeft { return } element.pressed = false - within := image.Point { x, y }.In(element.Bounds()) + within := image.Point { x, y }.In(element.entity.Bounds()) if time.Since(element.lastClick) < element.config.DoubleClickDelay() { if element.Enabled() && within && element.onChoose != nil { element.onChoose() @@ -138,29 +184,29 @@ func (element *File) HandleMouseUp (x, y int, button input.Button) { } else { element.lastClick = time.Now() } - element.drawAndPush() + element.entity.Invalidate() } // SetTheme sets the element's theme. -func (element *File) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawAndPush() +func (element *File) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.entity.Invalidate() } // SetConfig sets the element's configuration. -func (element *File) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - element.drawAndPush() +func (element *File) SetConfig (config tomo.Config) { + if config == element.config.Config { return } + element.config.Config = config + element.entity.Invalidate() } func (element *File) state () tomo.State { return tomo.State { Disabled: !element.Enabled(), - Focused: element.Focused(), + Focused: element.entity.Focused(), Pressed: element.pressed, - On: element.selected, + On: element.entity.Selected(), } } @@ -172,44 +218,11 @@ func (element *File) updateMinimumSize () { padding := element.theme.Padding(tomo.PatternButton) icon := element.icon() if icon == nil { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( padding.Horizontal(), padding.Vertical()) } else { bounds := padding.Inverse().Apply(icon.Bounds()) - element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) - } -} - -func (element *File) drawAndPush () { - if element.core.HasImage() { - element.drawAll() - element.core.DamageAll() - } -} - -func (element *File) drawAll () { - // background - state := element.state() - bounds := element.Bounds() - sink := element.theme.Sink(tomo.PatternButton) - element.theme. - Pattern(tomo.PatternButton, state). - Draw(element.core, bounds) - - // icon - icon := element.icon() - if icon != nil { - iconBounds := icon.Bounds() - offset := image.Pt ( - (bounds.Dx() - iconBounds.Dx()) / 2, - (bounds.Dy() - iconBounds.Dy()) / 2) - if element.pressed { - offset = offset.Add(sink) - } - icon.Draw ( - element.core, - element.theme.Color(tomo.ColorForeground, state), - bounds.Min.Add(offset)) + element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy()) } } diff --git a/elements/file/directory.go b/elements/file/directory.go deleted file mode 100644 index df352cf..0000000 --- a/elements/file/directory.go +++ /dev/null @@ -1,378 +0,0 @@ -package fileElements - -import "io/fs" -import "image" -import "path/filepath" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -type fileLayoutEntry struct { - *File - fs.DirEntry - Bounds image.Rectangle - Drawer textdraw.Drawer - TextPoint image.Point -} - -type historyEntry struct { - location string - filesystem ReadDirStatFS -} - -// Directory displays a list of files within a particular directory and -// file system. -type Directory struct { - *core.Core - *core.Propagator - core core.CoreControl - - children []fileLayoutEntry - scroll image.Point - contentBounds image.Rectangle - - config config.Wrapped - theme theme.Wrapped - - onScrollBoundsChange func () - - history []historyEntry - historyIndex int - onChoose func (file string) -} - -// NewDirectory creates a new directory view. If within is nil, it will use -// the OS file system. -func NewDirectory ( - location string, - within ReadDirStatFS, -) ( - element *Directory, - err error, -) { - element = &Directory { } - element.theme.Case = tomo.C("files", "directory") - element.Core, element.core = core.NewCore(element, element.redoAll) - element.Propagator = core.NewPropagator(element, element.core) - err = element.SetLocation(location, within) - return -} - -// Location returns the directory's location and filesystem. -func (element *Directory) Location () (string, ReadDirStatFS) { - if len(element.history) < 1 { return "", nil } - current := element.history[element.historyIndex] - return current.location, current.filesystem -} - -// SetLocation sets the directory's location and filesystem. If within is nil, -// it will use the OS file system. -func (element *Directory) SetLocation ( - location string, - within ReadDirStatFS, -) error { - if within == nil { - within = defaultFS { } - } - element.scroll = image.Point { } - - if element.history != nil { - element.historyIndex ++ - } - element.history = append ( - element.history[:element.historyIndex], - historyEntry { location, within }) - return element.Update() -} - -// Backward goes back a directory in history -func (element *Directory) Backward () (bool, error) { - if element.historyIndex > 1 { - element.historyIndex -- - return true, element.Update() - } else { - return false, nil - } -} - -// Forward goes forward a directory in history -func (element *Directory) Forward () (bool, error) { - if element.historyIndex < len(element.history) - 1 { - element.historyIndex ++ - return true, element.Update() - } else { - return false, nil - } -} - -// Update refreshes the directory's contents. -func (element *Directory) Update () error { - location, filesystem := element.Location() - entries, err := filesystem.ReadDir(location) - - // disown all entries - for _, file := range element.children { - file.DrawTo(nil, image.Rectangle { }, nil) - file.SetParent(nil) - - if file.Focused() { - file.HandleUnfocus() - } - } - - element.children = make([]fileLayoutEntry, len(entries)) - for index, entry := range entries { - filePath := filepath.Join(location, entry.Name()) - file, _ := NewFile(filePath, filesystem) - file.SetParent(element) - file.OnChoose (func () { - if element.onChoose != nil { - element.onChoose(filePath) - } - }) - element.children[index].File = file - element.children[index].DirEntry = entry - element.children[index].Drawer.SetFace (element.theme.FontFace( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.children[index].Drawer.SetText([]rune(entry.Name())) - element.children[index].Drawer.SetAlign(textdraw.AlignCenter) - } - - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } - return err -} - -// OnChoose sets a function to be called when the user double-clicks a file or -// sub-directory within the directory view. -func (element *Directory) OnChoose (callback func (file string)) { - element.onChoose = callback -} - -// CountChildren returns the amount of children contained within this element. -func (element *Directory) CountChildren () (count int) { - return len(element.children) -} - -// Child returns the child at the specified index. If the index is out of -// bounds, this method will return nil. -func (element *Directory) Child (index int) (child tomo.Element) { - if index < 0 || index > len(element.children) { return } - return element.children[index].File -} - -func (element *Directory) HandleMouseDown (x, y int, button input.Button) { - if button == input.ButtonLeft { - var file *File - for _, entry := range element.children { - if image.Pt(x, y).In(entry.Bounds) { - file = entry.File - } - } - if file != nil { - file.SetSelected(!file.Selected()) - } - } - element.Propagator.HandleMouseDown(x, y, button) -} - -func (element *Directory) redoAll () { - if !element.core.HasImage() { return } - - // do a layout - element.doLayout() - - maxScrollHeight := element.maxScrollHeight() - if element.scroll.Y > maxScrollHeight { - element.scroll.Y = maxScrollHeight - element.doLayout() - } - - // draw a background - rocks := make([]image.Rectangle, len(element.children)) - for index, entry := range element.children { - rocks[index] = entry.Bounds - } - pattern := element.theme.Pattern ( - tomo.PatternPinboard, - tomo.State { }) - artist.DrawShatter(element.core, pattern, element.Bounds(), rocks...) - - element.partition() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } - - // draw labels - foreground := element.theme.Color(tomo.ColorForeground, tomo.State { }) - for _, entry := range element.children { - entry.Drawer.Draw(element.core, foreground, entry.TextPoint) - } -} - -func (element *Directory) partition () { - for _, entry := range element.children { - entry.DrawTo(nil, entry.Bounds, nil) - } - - // cut our canvas up and give peices to child elements - for _, entry := range element.children { - if entry.Bounds.Overlaps(element.Bounds()) { - entry.DrawTo ( - canvas.Cut(element.core, entry.Bounds), - entry.Bounds, func (region image.Rectangle) { - element.core.DamageRegion(region) - }) - } - } -} - -func (element *Directory) Window () tomo.Window { - return element.core.Window() -} - -// NotifyMinimumSizeChange notifies the container that the minimum size of a -// child element has changed. -func (element *Directory) NotifyMinimumSizeChange (child tomo.Element) { - element.redoAll() - element.core.DamageAll() -} - -// SetTheme sets the element's theme. -func (element *Directory) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.Propagator.SetTheme(new) - element.redoAll() -} - -// SetConfig sets the element's configuration. -func (element *Directory) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.Propagator.SetConfig(new) - element.redoAll() -} -// ScrollContentBounds returns the full content size of the element. -func (element *Directory) ScrollContentBounds () image.Rectangle { - return element.contentBounds -} - -// ScrollViewportBounds returns the size and position of the element's -// viewport relative to ScrollBounds. -func (element *Directory) ScrollViewportBounds () image.Rectangle { - padding := element.theme.Padding(tomo.PatternPinboard) - bounds := padding.Apply(element.Bounds()) - bounds = bounds.Sub(bounds.Min).Add(element.scroll) - return bounds -} - -// ScrollTo scrolls the viewport to the specified point relative to -// ScrollBounds. -func (element *Directory) ScrollTo (position image.Point) { - if position.Y < 0 { - position.Y = 0 - } - maxScrollHeight := element.maxScrollHeight() - if position.Y > maxScrollHeight { - position.Y = maxScrollHeight - } - element.scroll = position - if element.core.HasImage() { - element.redoAll() - element.core.DamageAll() - } -} - -// OnScrollBoundsChange sets a function to be called when the element's viewport -// bounds, content bounds, or scroll axes change. -func (element *Directory) OnScrollBoundsChange (callback func ()) { - element.onScrollBoundsChange = callback -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *Directory) ScrollAxes () (horizontal, vertical bool) { - return false, true -} - -func (element *Directory) maxScrollHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) - viewportHeight := element.Bounds().Dy() - padding.Vertical() - height = element.contentBounds.Dy() - viewportHeight - if height < 0 { height = 0 } - return -} - -func (element *Directory) doLayout () { - margin := element.theme.Margin(tomo.PatternPinboard) - padding := element.theme.Padding(tomo.PatternPinboard) - bounds := padding.Apply(element.Bounds()) - element.contentBounds = image.Rectangle { } - - beginningOfRow := true - dot := bounds.Min.Sub(element.scroll) - rowHeight := 0 - for index, entry := range element.children { - width, height := entry.MinimumSize() - - if dot.X + width > bounds.Max.X { - dot.X = bounds.Min.Sub(element.scroll).X - dot.Y += rowHeight - if index > 1 { - dot.Y += margin.Y - } - beginningOfRow = true - } - - if beginningOfRow { - beginningOfRow = false - } else { - dot.X += margin.X - } - - entry.Drawer.SetMaxWidth(width) - bounds := image.Rect(dot.X, dot.Y, dot.X + width, dot.Y + height) - entry.Bounds = bounds - - drawerHeight := entry.Drawer.ReccomendedHeightFor(width) - entry.TextPoint = - image.Pt(bounds.Min.X, bounds.Max.Y + margin.Y). - Sub(entry.Drawer.LayoutBounds().Min) - bounds.Max.Y += margin.Y + drawerHeight - height += margin.Y + drawerHeight - if rowHeight < height { - rowHeight = height - } - - element.contentBounds = element.contentBounds.Union(bounds) - element.children[index] = entry - dot.X += width - } - - element.contentBounds = - element.contentBounds.Sub(element.contentBounds.Min) -} - -func (element *Directory) updateMinimumSize () { - padding := element.theme.Padding(tomo.PatternPinboard) - minimumWidth := 0 - for _, entry := range element.children { - width, _ := entry.MinimumSize() - if width > minimumWidth { - minimumWidth = width - } - } - element.core.SetMinimumSize ( - minimumWidth + padding.Horizontal(), - padding.Vertical()) -} diff --git a/elements/file/fs.go b/elements/fs.go similarity index 95% rename from elements/file/fs.go rename to elements/fs.go index 85572e3..88d9965 100644 --- a/elements/file/fs.go +++ b/elements/fs.go @@ -1,4 +1,4 @@ -package fileElements +package elements import "os" import "io/fs" diff --git a/elements/fun/clock.go b/elements/fun/clock.go index 23397e2..0b4468d 100644 --- a/elements/fun/clock.go +++ b/elements/fun/clock.go @@ -5,18 +5,14 @@ import "math" import "image" import "image/color" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" // AnalogClock can display the time of day in an analog format. type AnalogClock struct { - *core.Core - core core.CoreControl - time time.Time - - config config.Wrapped + entity tomo.Entity + time time.Time theme theme.Wrapped } @@ -24,46 +20,24 @@ type AnalogClock struct { func NewAnalogClock (newTime time.Time) (element *AnalogClock) { element = &AnalogClock { } element.theme.Case = tomo.C("tomo", "clock") - element.Core, element.core = core.NewCore(element, element.draw) - element.core.SetMinimumSize(64, 64) + element.entity = tomo.NewEntity(element) + element.entity.SetMinimumSize(64, 64) return } -// SetTime changes the time that the clock displays. -func (element *AnalogClock) SetTime (newTime time.Time) { - if newTime == element.time { return } - element.time = newTime - element.redo() +// Entity returns this element's entity. +func (element *AnalogClock) Entity () tomo.Entity { + return element.entity } -// SetTheme sets the element's theme. -func (element *AnalogClock) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.redo() -} - -// SetConfig sets the element's configuration. -func (element *AnalogClock) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - element.redo() -} - -func (element *AnalogClock) redo () { - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *AnalogClock) draw () { - bounds := element.Bounds() +// Draw causes the element to draw to the specified destination canvas. +func (element *AnalogClock) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() state := tomo.State { } pattern := element.theme.Pattern(tomo.PatternSunken, state) padding := element.theme.Padding(tomo.PatternSunken) - pattern.Draw(element.core, bounds) + pattern.Draw(destination, bounds) bounds = padding.Apply(bounds) @@ -72,6 +46,7 @@ func (element *AnalogClock) draw () { for hour := 0; hour < 12; hour ++ { element.radialLine ( + destination, foreground, 0.8, 0.9, float64(hour) / 6 * math.Pi) } @@ -80,25 +55,40 @@ func (element *AnalogClock) draw () { minute := float64(element.time.Minute()) + second / 60 hour := float64(element.time.Hour()) + minute / 60 - element.radialLine(foreground, 0, 0.5, (hour - 3) / 6 * math.Pi) - element.radialLine(foreground, 0, 0.7, (minute - 15) / 30 * math.Pi) - element.radialLine(accent, 0, 0.7, (second - 15) / 30 * math.Pi) + element.radialLine(destination, foreground, 0, 0.5, (hour - 3) / 6 * math.Pi) + element.radialLine(destination, foreground, 0, 0.7, (minute - 15) / 30 * math.Pi) + element.radialLine(destination, accent, 0, 0.7, (second - 15) / 30 * math.Pi) +} + +// SetTime changes the time that the clock displays. +func (element *AnalogClock) SetTime (newTime time.Time) { + if newTime == element.time { return } + element.time = newTime + element.entity.Invalidate() +} + +// SetTheme sets the element's theme. +func (element *AnalogClock) SetTheme (new tomo.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.entity.Invalidate() } func (element *AnalogClock) radialLine ( + destination canvas.Canvas, source color.RGBA, inner float64, outer float64, radian float64, ) { - bounds := element.Bounds() + bounds := element.entity.Bounds() width := float64(bounds.Dx()) / 2 height := float64(bounds.Dy()) / 2 - min := element.Bounds().Min.Add(image.Pt ( + min := bounds.Min.Add(image.Pt ( int(math.Cos(radian) * inner * width + width), int(math.Sin(radian) * inner * height + height))) - max := element.Bounds().Min.Add(image.Pt ( + max := bounds.Min.Add(image.Pt ( int(math.Cos(radian) * outer * width + width), int(math.Sin(radian) * outer * height + height))) - shapes.ColorLine(element.core, source, 1, min, max) + shapes.ColorLine(destination, source, 1, min, max) } diff --git a/elements/fun/piano.go b/elements/fun/piano.go index 9097fc7..5776ed1 100644 --- a/elements/fun/piano.go +++ b/elements/fun/piano.go @@ -3,8 +3,8 @@ package fun import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" import "git.tebibyte.media/sashakoshka/tomo/elements/fun/music" @@ -18,21 +18,19 @@ type pianoKey struct { // Piano is an element that can be used to input midi notes. type Piano struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl - low, high music.Octave - + entity tomo.FocusableEntity + config config.Wrapped theme theme.Wrapped flatTheme theme.Wrapped sharpTheme theme.Wrapped + low, high music.Octave flatKeys []pianoKey sharpKeys []pianoKey contentBounds image.Rectangle + enabled bool pressed *pianoKey keynavPressed map[music.Note] bool @@ -43,11 +41,7 @@ type Piano struct { // NewPiano returns a new piano element with a lowest and highest octave, // inclusive. If low is greater than high, they will be swapped. func NewPiano (low, high music.Octave) (element *Piano) { - if low > high { - temp := low - low = high - high = temp - } + if low > high { low, high = high, low } element = &Piano { low: low, @@ -58,16 +52,68 @@ func NewPiano (low, high music.Octave) (element *Piano) { element.theme.Case = tomo.C("tomo", "piano") element.flatTheme.Case = tomo.C("tomo", "piano", "flatKey") element.sharpTheme.Case = tomo.C("tomo", "piano", "sharpKey") - element.Core, element.core = core.NewCore (element, func () { - element.recalculate() - element.draw() - }) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.redo) + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.updateMinimumSize() return } +// Entity returns this element's entity. +func (element *Piano) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Piano) Draw (destination canvas.Canvas) { + element.recalculate() + + state := tomo.State { + Focused: element.entity.Focused(), + Disabled: !element.Enabled(), + } + + for _, key := range element.flatKeys { + _, keynavPressed := element.keynavPressed[key.Note] + element.drawFlat ( + destination, + key.Rectangle, + element.pressed != nil && + (*element.pressed).Note == key.Note || keynavPressed, + state) + } + for _, key := range element.sharpKeys { + _, keynavPressed := element.keynavPressed[key.Note] + element.drawSharp ( + destination, + key.Rectangle, + element.pressed != nil && + (*element.pressed).Note == key.Note || keynavPressed, + state) + } + + pattern := element.theme.Pattern(tomo.PatternPinboard, state) + artist.DrawShatter ( + destination, pattern, element.entity.Bounds(), + element.contentBounds) +} + +// Focus gives this element input focus. +func (element *Piano) Focus () { + element.entity.Focus() +} + +// Enabled returns whether this piano can be played or not. +func (element *Piano) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this piano can be played or not. +func (element *Piano) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + + // OnPress sets a function to be called when a key is pressed. func (element *Piano) OnPress (callback func (note music.Note)) { element.onPress = callback @@ -90,7 +136,7 @@ func (element *Piano) HandleMouseUp (x, y int, button input.Button) { element.onRelease((*element.pressed).Note) } element.pressed = nil - element.redo() + element.entity.Invalidate() } func (element *Piano) HandleMotion (x, y int) { @@ -126,7 +172,7 @@ func (element *Piano) pressUnderMouseCursor (point image.Point) { if element.onPress != nil { element.onPress((*element.pressed).Note) } - element.redo() + element.entity.Invalidate() } } @@ -186,7 +232,7 @@ func (element *Piano) HandleKeyDown (key input.Key, modifiers input.Modifiers) { if element.onPress != nil { element.onPress(note) } - element.redo() + element.entity.Invalidate() } } @@ -199,7 +245,7 @@ func (element *Piano) HandleKeyUp (key input.Key, modifiers input.Modifiers) { if element.onRelease != nil { element.onRelease(note) } - element.redo() + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -209,8 +255,7 @@ func (element *Piano) SetTheme (new tomo.Theme) { element.flatTheme.Theme = new element.sharpTheme.Theme = new element.updateMinimumSize() - element.recalculate() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -218,13 +263,12 @@ func (element *Piano) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.recalculate() - element.redo() + element.entity.Invalidate() } func (element *Piano) updateMinimumSize () { padding := element.theme.Padding(tomo.PatternPinboard) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( pianoKeyWidth * 7 * element.countOctaves() + padding.Horizontal(), 64 + padding.Vertical()) @@ -242,19 +286,12 @@ func (element *Piano) countSharps () int { return element.countOctaves() * 5 } -func (element *Piano) redo () { - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - func (element *Piano) recalculate () { element.flatKeys = make([]pianoKey, element.countFlats()) element.sharpKeys = make([]pianoKey, element.countSharps()) - padding := element.theme.Padding(tomo.PatternPinboard) - bounds := padding.Apply(element.Bounds()) + padding := element.theme.Padding(tomo.PatternPinboard) + bounds := padding.Apply(element.entity.Bounds()) dot := bounds.Min note := element.low.Note(0) @@ -285,50 +322,24 @@ func (element *Piano) recalculate () { } } -func (element *Piano) draw () { - state := tomo.State { - Focused: element.Focused(), - Disabled: !element.Enabled(), - } - - for _, key := range element.flatKeys { - _, keynavPressed := element.keynavPressed[key.Note] - element.drawFlat ( - key.Rectangle, - element.pressed != nil && - (*element.pressed).Note == key.Note || keynavPressed, - state) - } - for _, key := range element.sharpKeys { - _, keynavPressed := element.keynavPressed[key.Note] - element.drawSharp ( - key.Rectangle, - element.pressed != nil && - (*element.pressed).Note == key.Note || keynavPressed, - state) - } - - pattern := element.theme.Pattern(tomo.PatternPinboard, state) - artist.DrawShatter ( - element.core, pattern, element.Bounds(), element.contentBounds) -} - func (element *Piano) drawFlat ( + destination canvas.Canvas, bounds image.Rectangle, pressed bool, state tomo.State, ) { state.Pressed = pressed pattern := element.flatTheme.Pattern(tomo.PatternButton, state) - pattern.Draw(element.core, bounds) + pattern.Draw(destination, bounds) } func (element *Piano) drawSharp ( + destination canvas.Canvas, bounds image.Rectangle, pressed bool, state tomo.State, ) { state.Pressed = pressed pattern := element.sharpTheme.Pattern(tomo.PatternButton, state) - pattern.Draw(element.core, bounds) + pattern.Draw(destination, bounds) } diff --git a/elements/icon.go b/elements/icon.go index ba52d8a..03f0e9c 100644 --- a/elements/icon.go +++ b/elements/icon.go @@ -2,47 +2,72 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" +// Icon is an element capable of displaying a singular icon. type Icon struct { - *core.Core - core core.CoreControl - theme theme.Wrapped - id tomo.Icon - size tomo.IconSize + entity tomo.Entity + theme theme.Wrapped + id tomo.Icon + size tomo.IconSize } +// Icon creates a new icon element. func NewIcon (id tomo.Icon, size tomo.IconSize) (element *Icon) { element = &Icon { id: id, size: size, } + element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "icon") - element.Core, element.core = core.NewCore(element, element.draw) element.updateMinimumSize() return } +// Entity returns this element's entity. +func (element *Icon) Entity () tomo.Entity { + return element.entity +} + +// SetIcon sets the element's icon. func (element *Icon) SetIcon (id tomo.Icon, size tomo.IconSize) { element.id = id element.size = size + if element.entity == nil { return } element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. func (element *Icon) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new + if element.entity == nil { return } element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() + element.entity.Invalidate() +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Icon) Draw (destination canvas.Canvas) { + if element.entity == nil { return } + + bounds := element.entity.Bounds() + state := tomo.State { } + element.theme. + Pattern(tomo.PatternBackground, state). + Draw(destination, bounds) + icon := element.icon() + if icon != nil { + iconBounds := icon.Bounds() + offset := image.Pt ( + (bounds.Dx() - iconBounds.Dx()) / 2, + (bounds.Dy() - iconBounds.Dy()) / 2) + icon.Draw ( + destination, + element.theme.Color(tomo.ColorForeground, state), + bounds.Min.Add(offset)) } } @@ -53,28 +78,9 @@ func (element *Icon) icon () artist.Icon { func (element *Icon) updateMinimumSize () { icon := element.icon() if icon == nil { - element.core.SetMinimumSize(0, 0) + element.entity.SetMinimumSize(0, 0) } else { bounds := icon.Bounds() - element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) - } -} - -func (element *Icon) draw () { - bounds := element.Bounds() - state := tomo.State { } - element.theme. - Pattern(tomo.PatternBackground, state). - Draw(element.core, bounds) - icon := element.icon() - if icon != nil { - iconBounds := icon.Bounds() - offset := image.Pt ( - (bounds.Dx() - iconBounds.Dx()) / 2, - (bounds.Dy() - iconBounds.Dy()) / 2) - icon.Draw ( - element.core, - element.theme.Color(tomo.ColorForeground, state), - bounds.Min.Add(offset)) + element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy()) } } diff --git a/elements/image.go b/elements/image.go index ab5d767..7ad3122 100644 --- a/elements/image.go +++ b/elements/image.go @@ -1,25 +1,35 @@ package elements import "image" +import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" +// TODO: this element is lame need to make it better + +// Image is an element capable of displaying an image. type Image struct { - *core.Core - core core.CoreControl + entity tomo.Entity buffer canvas.Canvas } +// NewImage creates a new image element. func NewImage (image image.Image) (element *Image) { element = &Image { buffer: canvas.FromImage(image) } - element.Core, element.core = core.NewCore(element, element.draw) - bounds := image.Bounds() - element.core.SetMinimumSize(bounds.Dx(), bounds.Dy()) + element.entity = tomo.NewEntity(element) + bounds := element.buffer.Bounds() + element.entity.SetMinimumSize(bounds.Dx(), bounds.Dy()) return } -func (element *Image) draw () { - (patterns.Texture { Canvas: element.buffer }). - Draw(element.core, element.Bounds()) +// Entity returns this element's entity. +func (element *Image) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Image) Draw (destination canvas.Canvas) { + if element.entity == nil { return } + (patterns.Texture { Canvas: element.buffer }). + Draw(destination, element.entity.Bounds()) } diff --git a/elements/label.go b/elements/label.go index 412d636..f48b088 100644 --- a/elements/label.go +++ b/elements/label.go @@ -2,16 +2,15 @@ package elements import "golang.org/x/image/math/fixed" import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Label is a simple text box. type Label struct { - *core.Core - core core.CoreControl - + entity tomo.FlexibleEntity + align textdraw.Align wrap bool text string @@ -19,50 +18,34 @@ type Label struct { forcedColumns int forcedRows int + minHeight int config config.Wrapped theme theme.Wrapped - - onFlexibleHeightChange func () } -// NewLabel creates a new label. If wrap is set to true, the text inside will be -// wrapped. -func NewLabel (text string, wrap bool) (element *Label) { +// NewLabel creates a new label. +func NewLabel (text string) (element *Label) { element = &Label { } element.theme.Case = tomo.C("tomo", "label") - element.Core, element.core = core.NewCore(element, element.handleResize) + element.entity = tomo.NewEntity(element).(tomo.FlexibleEntity) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) - element.SetWrap(wrap) element.SetText(text) return } -func (element *Label) redo () { - face := element.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal) - element.drawer.SetFace(face) - element.updateMinimumSize() - bounds := element.Bounds() - if element.wrap { - element.drawer.SetMaxWidth(bounds.Dx()) - element.drawer.SetMaxHeight(bounds.Dy()) - } - element.draw() - element.core.DamageAll() +// NewLabelWrapped creates a new label with text wrapping on. +func NewLabelWrapped (text string) (element *Label) { + element = NewLabel(text) + element.SetWrap(true) + return } -func (element *Label) handleResize () { - bounds := element.Bounds() - if element.wrap { - element.drawer.SetMaxWidth(bounds.Dx()) - element.drawer.SetMaxHeight(bounds.Dy()) - } - element.draw() - return +// Entity returns this element's entity. +func (element *Label) Entity () tomo.Entity { + return element.entity } // EmCollapse forces a minimum width and height upon the label. The width is @@ -82,17 +65,10 @@ func (element *Label) FlexibleHeightFor (width int) (height int) { if element.wrap { return element.drawer.ReccomendedHeightFor(width) } else { - _, height = element.MinimumSize() - return + return element.minHeight } } -// OnFlexibleHeightChange sets a function to be called when the parameters -// affecting this element's flexible height are changed. -func (element *Label) OnFlexibleHeightChange (callback func ()) { - element.onFlexibleHeightChange = callback -} - // SetText sets the label's text. func (element *Label) SetText (text string) { if element.text == text { return } @@ -100,11 +76,7 @@ func (element *Label) SetText (text string) { element.text = text element.drawer.SetText([]rune(text)) element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetWrap sets wether or not the label's text wraps. If the text is set to @@ -119,25 +91,16 @@ func (element *Label) SetWrap (wrap bool) { } element.wrap = wrap element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetAlign sets the alignment method of the label. func (element *Label) SetAlign (align textdraw.Align) { if align == element.align { return } - element.align = align element.drawer.SetAlign(align) element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -148,11 +111,7 @@ func (element *Label) SetTheme (new tomo.Theme) { tomo.FontStyleRegular, tomo.FontSizeNormal)) element.updateMinimumSize() - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -160,11 +119,25 @@ func (element *Label) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() + element.entity.Invalidate() +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Label) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() - if element.core.HasImage () { - element.draw() - element.core.DamageAll() + if element.wrap { + element.drawer.SetMaxWidth(bounds.Dx()) + element.drawer.SetMaxHeight(bounds.Dy()) } + + element.entity.DrawBackground(destination) + + textBounds := element.drawer.LayoutBounds() + foreground := element.theme.Color ( + tomo.ColorForeground, + tomo.State { }) + element.drawer.Draw(destination, foreground, bounds.Min.Sub(textBounds.Min)) } func (element *Label) updateMinimumSize () { @@ -176,9 +149,7 @@ func (element *Label) updateMinimumSize () { em = element.theme.Padding(tomo.PatternBackground)[0] } width, height = em, element.drawer.LineHeight().Round() - if element.onFlexibleHeightChange != nil { - element.onFlexibleHeightChange() - } + element.entity.NotifyFlexibleHeightChange() } else { bounds := element.drawer.LayoutBounds() width, height = bounds.Dx(), bounds.Dy() @@ -196,18 +167,6 @@ func (element *Label) updateMinimumSize () { Mul(fixed.I(element.forcedRows)).Floor() } - element.core.SetMinimumSize(width, height) -} - -func (element *Label) draw () { - element.core.DrawBackground ( - element.theme.Pattern(tomo.PatternBackground, tomo.State { })) - - bounds := element.Bounds() - textBounds := element.drawer.LayoutBounds() - - foreground := element.theme.Color ( - tomo.ColorForeground, - tomo.State { }) - element.drawer.Draw(element.core, foreground, bounds.Min.Sub(textBounds.Min)) + element.minHeight = height + element.entity.SetMinimumSize(width, height) } diff --git a/elements/lerpslider.go b/elements/lerpslider.go index f55f190..b230b0f 100644 --- a/elements/lerpslider.go +++ b/elements/lerpslider.go @@ -15,16 +15,20 @@ type LerpSlider[T Numeric] struct { max T } -// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. If -// vertical is set to true, the slider will be vertical instead of horizontal. -func NewLerpSlider[T Numeric] (min, max T, value T, vertical bool) (element *LerpSlider[T]) { +// NewLerpSlider creates a new LerpSlider with a minimum and maximum value. +func NewLerpSlider[T Numeric] ( + min, max T, value T, + orientation Orientation, +) ( + element *LerpSlider[T], +) { if min > max { temp := max max = min min = temp } element = &LerpSlider[T] { - Slider: NewSlider(0, vertical), + Slider: NewSlider(0, orientation), min: min, max: max, } diff --git a/elements/list.go b/elements/list.go index fb03f7c..b7429f6 100644 --- a/elements/list.go +++ b/elements/list.go @@ -1,107 +1,148 @@ package elements -import "fmt" import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" -// List is an element that contains several objects that a user can select. +type listEntity interface { + tomo.ContainerEntity + tomo.ScrollableEntity +} + type List struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl - - pressed bool + container + entity listEntity + + scroll image.Point + contentBounds image.Rectangle + columnSizes []int + selected int - contentHeight int forcedMinimumWidth int forcedMinimumHeight int + + theme theme.Wrapped - selectedEntry int - scroll int - entries []ListEntry - - config config.Wrapped - theme theme.Wrapped - - onNoEntrySelected func () onScrollBoundsChange func () } -// NewList creates a new list element with the specified entries. -func NewList (entries ...ListEntry) (element *List) { - element = &List { selectedEntry: -1 } +func NewList (columns int, children ...tomo.Element) (element *List) { + if columns < 1 { columns = 1 } + element = &List { selected: -1 } + element.columnSizes = make([]int, columns) element.theme.Case = tomo.C("tomo", "list") - element.Core, element.core = core.NewCore(element, element.handleResize) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore (element.core, func () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } - }) - - element.entries = make([]ListEntry, len(entries)) - for index, entry := range entries { - element.entries[index] = entry - } - - element.updateMinimumSize() + element.entity = tomo.NewEntity(element).(listEntity) + element.container.entity = element.entity + element.minimumSize = element.updateMinimumSize + element.init() + element.Adopt(children...) return } -func (element *List) handleResize () { - for index, entry := range element.entries { - element.entries[index] = element.resizeEntryToFit(entry) +func (element *List) Draw (destination canvas.Canvas) { + rocks := make([]image.Rectangle, element.entity.CountChildren()) + for index := 0; index < element.entity.CountChildren(); index ++ { + rocks[index] = element.entity.Child(index).Entity().Bounds() } - if element.scroll > element.maxScrollHeight() { - element.scroll = element.maxScrollHeight() + pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { }) + artist.DrawShatter(destination, pattern, element.entity.Bounds(), rocks...) +} + +func (element *List) Layout () { + if element.scroll.Y > element.maxScrollHeight() { + element.scroll.Y = element.maxScrollHeight() } - element.draw() - element.scrollBoundsChange() + + margin := element.theme.Margin(tomo.PatternSunken) + padding := element.theme.Padding(tomo.PatternSunken) + bounds := padding.Apply(element.entity.Bounds()) + element.contentBounds = image.Rectangle { } + + dot := bounds.Min.Sub(element.scroll) + xStart := dot.X + rowHeight := 0 + columnIndex := 0 + nextLine := func () { + dot.X = xStart + dot.Y += margin.Y + dot.Y += rowHeight + rowHeight = 0 + columnIndex = 0 + } + + for index := 0; index < element.entity.CountChildren(); index ++ { + child := element.entity.Child(index) + entry := element.scratch[child] + + if columnIndex >= len(element.columnSizes) { + nextLine() + } + width := element.columnSizes[columnIndex] + height := int(entry.minSize) + + if len(element.columnSizes) == 1 && width < bounds.Dx() { + width = bounds.Dx() + } + + if rowHeight < height { + rowHeight = height + } + + childBounds := tomo.Bounds ( + dot.X, dot.Y, + width, height) + element.entity.PlaceChild(index, childBounds) + element.contentBounds = element.contentBounds.Union(childBounds) + + dot.X += width + margin.X + + columnIndex ++ + } + + element.contentBounds = + element.contentBounds.Sub(element.contentBounds.Min) + + element.entity.NotifyScrollBoundsChange() + if element.onScrollBoundsChange != nil { + element.onScrollBoundsChange() + } +} + +func (element *List) HandleChildMouseDown (x, y int, button input.Button, child tomo.Element) { + if child, ok := child.(tomo.Selectable); ok { + index := element.entity.IndexOf(child) + if element.selected == index { return } + if element.selected >= 0 { + element.entity.SelectChild(element.selected, false) + } + element.selected = index + element.entity.SelectChild(index, true) + } +} + +func (element *List) HandleChildMouseUp (int, int, input.Button, tomo.Element) { } + +func (element *List) HandleChildFlexibleHeightChange (child tomo.Flexible) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *List) DrawBackground (destination canvas.Canvas) { + element.entity.DrawBackground(destination) } // SetTheme sets the element's theme. -func (element *List) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - for index, entry := range element.entries { - entry.SetTheme(element.theme.Theme) - element.entries[index] = entry - } +func (element *List) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme element.updateMinimumSize() - element.redo() -} - -// SetConfig sets the element's configuration. -func (element *List) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - for index, entry := range element.entries { - entry.SetConfig(element.config) - element.entries[index] = entry - } - element.updateMinimumSize() - element.redo() -} - -func (element *List) redo () { - for index, entry := range element.entries { - element.entries[index] = element.resizeEntryToFit(entry) - } - - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() + element.entity.Invalidate() + element.entity.InvalidateLayout() } // Collapse forces a minimum width and height upon the list. If a zero value is @@ -119,118 +160,39 @@ func (element *List) Collapse (width, height int) { element.forcedMinimumWidth = width element.forcedMinimumHeight = height + element.updateMinimumSize() - - for index, entry := range element.entries { - element.entries[index] = element.resizeEntryToFit(entry) - } - - element.redo() + element.entity.Invalidate() + element.entity.InvalidateLayout() } -func (element *List) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - if !element.Focused() { element.Focus() } - if button != input.ButtonLeft { return } - element.pressed = true - if element.selectUnderMouse(x, y) && element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *List) HandleMouseUp (x, y int, button input.Button) { - if button != input.ButtonLeft { return } - element.pressed = false -} - -func (element *List) HandleMotion (x, y int) { - if element.pressed { - if element.selectUnderMouse(x, y) && element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - } -} - -func (element *List) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if !element.Enabled() { return } - - altered := false - switch key { - case input.KeyLeft, input.KeyUp: - altered = element.changeSelectionBy(-1) - - case input.KeyRight, input.KeyDown: - altered = element.changeSelectionBy(1) - - case input.KeyEscape: - altered = element.selectEntry(-1) - } - - if altered && element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *List) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } - // ScrollContentBounds returns the full content size of the element. -func (element *List) ScrollContentBounds () (bounds image.Rectangle) { - return image.Rect ( - 0, 0, - 1, element.contentHeight) +func (element *List) ScrollContentBounds () image.Rectangle { + return element.contentBounds } -// ScrollViewportBounds returns the size and position of the element's viewport -// relative to ScrollBounds. -func (element *List) ScrollViewportBounds () (bounds image.Rectangle) { - return image.Rect ( - 0, element.scroll, - 0, element.scroll + element.scrollViewportHeight()) +// ScrollViewportBounds returns the size and position of the element's +// viewport relative to ScrollBounds. +func (element *List) ScrollViewportBounds () image.Rectangle { + padding := element.theme.Padding(tomo.PatternBackground) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min).Add(element.scroll) + return bounds } // ScrollTo scrolls the viewport to the specified point relative to // ScrollBounds. func (element *List) ScrollTo (position image.Point) { - element.scroll = position.Y - if element.scroll < 0 { - element.scroll = 0 - } else if element.scroll > element.maxScrollHeight() { - element.scroll = element.maxScrollHeight() + if position.Y < 0 { + position.Y = 0 } - - if element.core.HasImage () { - element.draw() - element.core.DamageAll() + maxScrollHeight := element.maxScrollHeight() + if position.Y > maxScrollHeight { + position.Y = maxScrollHeight } - element.scrollBoundsChange() -} - -// ScrollAxes returns the supported axes for scrolling. -func (element *List) ScrollAxes () (horizontal, vertical bool) { - return false, true -} - -func (element *List) scrollViewportHeight () (height int) { - padding := element.theme.Padding(tomo.PatternSunken) - return element.Bounds().Dy() - padding[0] - padding[2] -} - -func (element *List) maxScrollHeight () (height int) { - height = - element.contentHeight - - element.scrollViewportHeight() - if height < 0 { height = 0 } - return -} - -// OnNoEntrySelected sets a function to be called when the user chooses to -// deselect the current selected entry by clicking on empty space within the -// list or by pressing the escape key. -func (element *List) OnNoEntrySelected (callback func ()) { - element.onNoEntrySelected = callback + element.scroll = position + element.entity.Invalidate() + element.entity.InvalidateLayout() } // OnScrollBoundsChange sets a function to be called when the element's viewport @@ -239,239 +201,73 @@ func (element *List) OnScrollBoundsChange (callback func ()) { element.onScrollBoundsChange = callback } -// CountEntries returns the amount of entries in the list. -func (element *List) CountEntries () (count int) { - return len(element.entries) +// ScrollAxes returns the supported axes for scrolling. +func (element *List) ScrollAxes () (horizontal, vertical bool) { + return false, true } -// Append adds one or more entries to the end of the list. -func (element *List) Append (entries ...ListEntry) { - // append - for index, entry := range entries { - entry = element.resizeEntryToFit(entry) - entry.SetTheme(element.theme.Theme) - entry.SetConfig(element.config) - entries[index] = entry - } - element.entries = append(element.entries, entries...) - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// EntryAt returns the entry at the specified index. If the index is out of -// bounds, it panics. -func (element *List) EntryAt (index int) (entry ListEntry) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.EntryAt index out of range: ", index)) - } - return element.entries[index] -} - -// Insert inserts an entry into the list at the speified index. If the index is -// out of bounds, it is constrained either to zero or len(entries). -func (element *List) Insert (index int, entry ListEntry) { - if index < 0 { index = 0 } - if index > len(element.entries) { index = len(element.entries) } - - // insert - element.entries = append ( - element.entries[:index + 1], - element.entries[index:]...) - entry = element.resizeEntryToFit(entry) - element.entries[index] = entry - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Remove removes the entry at the specified index. If the index is out of -// bounds, it panics. -func (element *List) Remove (index int) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.Remove index out of range: ", index)) - } - - // delete - element.entries = append ( - element.entries[:index], - element.entries[index + 1:]...) - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Clear removes all entries from the list. -func (element *List) Clear () { - element.entries = nil - - // recalculate, redraw, notify - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Replace replaces the entry at the specified index with another. If the index -// is out of bounds, it panics. -func (element *List) Replace (index int, entry ListEntry) { - if index < 0 || index >= len(element.entries) { - panic(fmt.Sprint("basic.List.Replace index out of range: ", index)) - } - - // replace - entry = element.resizeEntryToFit(entry) - element.entries[index] = entry - - // redraw - element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - element.scrollBoundsChange() -} - -// Select selects a specific item in the list. If the index is out of bounds, -// no items will be selecected. -func (element *List) Select (index int) { - if element.selectEntry(index) { - element.redo() - } -} - -func (element *List) selectUnderMouse (x, y int) (updated bool) { +func (element *List) maxScrollHeight () (height int) { padding := element.theme.Padding(tomo.PatternSunken) - bounds := padding.Apply(element.Bounds()) - mousePoint := image.Pt(x, y) - dot := image.Pt ( - bounds.Min.X, - bounds.Min.Y - element.scroll) - - newlySelectedEntryIndex := -1 - for index, entry := range element.entries { - entryPosition := dot - dot.Y += entry.Bounds().Dy() - if entryPosition.Y > bounds.Max.Y { break } - if mousePoint.In(entry.Bounds().Add(entryPosition)) { - newlySelectedEntryIndex = index - break - } - } - - return element.selectEntry(newlySelectedEntryIndex) -} - -func (element *List) selectEntry (index int) (updated bool) { - if element.selectedEntry == index { return false } - element.selectedEntry = index - if element.selectedEntry < 0 { - if element.onNoEntrySelected != nil { - element.onNoEntrySelected() - } - } else { - element.entries[element.selectedEntry].RunSelect() - } - return true -} - -func (element *List) changeSelectionBy (delta int) (updated bool) { - newIndex := element.selectedEntry + delta - if newIndex < 0 { newIndex = len(element.entries) - 1 } - if newIndex >= len(element.entries) { newIndex = 0 } - return element.selectEntry(newIndex) -} - -func (element *List) resizeEntryToFit (entry ListEntry) (resized ListEntry) { - bounds := element.Bounds() - padding := element.theme.Padding(tomo.PatternSunken) - entry.Resize(padding.Apply(bounds).Dx()) - return entry + viewportHeight := element.entity.Bounds().Dy() - padding.Vertical() + height = element.contentBounds.Dy() - viewportHeight + if height < 0 { height = 0 } + return } func (element *List) updateMinimumSize () { - element.contentHeight = 0 - for _, entry := range element.entries { - element.contentHeight += entry.Bounds().Dy() - } - - minimumWidth := element.forcedMinimumWidth - minimumHeight := element.forcedMinimumHeight - - if minimumWidth == 0 { - for _, entry := range element.entries { - entryWidth := entry.MinimumWidth() - if entryWidth > minimumWidth { - minimumWidth = entryWidth - } - } - } - - if minimumHeight == 0 { - minimumHeight = element.contentHeight - } - + margin := element.theme.Margin(tomo.PatternSunken) padding := element.theme.Padding(tomo.PatternSunken) - minimumHeight += padding[0] + padding[2] - element.core.SetMinimumSize(minimumWidth, minimumHeight) -} - -func (element *List) scrollBoundsChange () { - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - if element.onScrollBoundsChange != nil { - element.onScrollBoundsChange() - } -} - -func (element *List) draw () { - bounds := element.Bounds() - padding := element.theme.Padding(tomo.PatternSunken) - innerBounds := padding.Apply(bounds) - state := tomo.State { - Disabled: !element.Enabled(), - Focused: element.Focused(), - } - - dot := image.Point { - innerBounds.Min.X, - innerBounds.Min.Y - element.scroll, - } - innerCanvas := canvas.Cut(element.core, innerBounds) - for index, entry := range element.entries { - entryPosition := dot - dot.Y += entry.Bounds().Dy() - if dot.Y < innerBounds.Min.Y { continue } - if entryPosition.Y > innerBounds.Max.Y { break } - entry.Draw ( - innerCanvas, entryPosition, - element.Focused(), element.selectedEntry == index) - } - - covered := image.Rect ( - 0, 0, - innerBounds.Dx(), element.contentHeight, - ).Add(innerBounds.Min).Intersect(innerBounds) - pattern := element.theme.Pattern(tomo.PatternSunken, state) - artist.DrawShatter ( - element.core, pattern, bounds, covered) + for index := range element.columnSizes { + element.columnSizes[index] = 0 + } + + height := 0 + rowHeight := 0 + columnIndex := 0 + nextLine := func () { + height += rowHeight + rowHeight = 0 + columnIndex = 0 + } + for index := 0; index < element.entity.CountChildren(); index ++ { + if columnIndex >= len(element.columnSizes) { + if index > 0 { height += margin.Y } + nextLine() + } + + child := element.entity.Child(index) + entry := element.scratch[child] + + entryWidth, entryHeight := element.entity.ChildMinimumSize(index) + entry.minBreadth = float64(entryWidth) + entry.minSize = float64(entryHeight) + element.scratch[child] = entry + + if rowHeight < entryHeight { + rowHeight = entryHeight + } + if element.columnSizes[columnIndex] < entryWidth { + element.columnSizes[columnIndex] = entryWidth + } + + columnIndex ++ + } + nextLine() + + width := 0; for index, size := range element.columnSizes { + width += size + if index > 0 { width += margin.X } + } + width += padding.Horizontal() + height += padding.Vertical() + + if element.forcedMinimumHeight > 0 { + height = element.forcedMinimumHeight + } + if element.forcedMinimumWidth > 0 { + width = element.forcedMinimumWidth + } + + element.entity.SetMinimumSize(width, height) } diff --git a/elements/listentry.go b/elements/listentry.go deleted file mode 100644 index d98cf37..0000000 --- a/elements/listentry.go +++ /dev/null @@ -1,104 +0,0 @@ -package elements - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/default/theme" -import "git.tebibyte.media/sashakoshka/tomo/default/config" - -// ListEntry is an item that can be added to a list. -type ListEntry struct { - drawer textdraw.Drawer - bounds image.Rectangle - text string - width int - minimumWidth int - - config config.Wrapped - theme theme.Wrapped - - onSelect func () -} - -func NewListEntry (text string, onSelect func ()) (entry ListEntry) { - entry = ListEntry { - text: text, - onSelect: onSelect, - } - entry.theme.Case = tomo.C("tomo", "listEntry") - entry.drawer.SetFace (entry.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - entry.drawer.SetText([]rune(text)) - entry.updateBounds() - return -} - -func (entry *ListEntry) SetTheme (new tomo.Theme) { - if new == entry.theme.Theme { return } - entry.theme.Theme = new - entry.drawer.SetFace (entry.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - entry.updateBounds() -} - -func (entry *ListEntry) SetConfig (new tomo.Config) { - if new == entry.config.Config { return } - entry.config.Config = new -} - -func (entry *ListEntry) updateBounds () { - padding := entry.theme.Padding(tomo.PatternRaised) - entry.bounds = padding.Inverse().Apply(entry.drawer.LayoutBounds()) - entry.bounds = entry.bounds.Sub(entry.bounds.Min) - entry.minimumWidth = entry.bounds.Dx() - entry.bounds.Max.X = entry.width -} - -func (entry *ListEntry) Draw ( - destination canvas.Canvas, - offset image.Point, - focused bool, - on bool, -) ( - updatedRegion image.Rectangle, -) { - state := tomo.State { - Focused: focused, - On: on, - } - - pattern := entry.theme.Pattern(tomo.PatternRaised, state) - padding := entry.theme.Padding(tomo.PatternRaised) - bounds := entry.Bounds().Add(offset) - pattern.Draw(destination, bounds) - - foreground := entry.theme.Color (tomo.ColorForeground, state) - return entry.drawer.Draw ( - destination, - foreground, - offset.Add(image.Pt(padding[artist.SideLeft], padding[artist.SideTop])). - Sub(entry.drawer.LayoutBounds().Min)) -} - -func (entry *ListEntry) RunSelect () { - if entry.onSelect != nil { - entry.onSelect() - } -} - -func (entry *ListEntry) Bounds () (bounds image.Rectangle) { - return entry.bounds -} - -func (entry *ListEntry) Resize (width int) { - entry.width = width - entry.updateBounds() -} - -func (entry *ListEntry) MinimumWidth () (width int) { - return entry.minimumWidth -} diff --git a/elements/progressbar.go b/elements/progressbar.go index 3286214..5c96daa 100644 --- a/elements/progressbar.go +++ b/elements/progressbar.go @@ -2,14 +2,13 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // ProgressBar displays a visual indication of how far along a task is. type ProgressBar struct { - *core.Core - core core.CoreControl + entity tomo.Entity progress float64 config config.Wrapped @@ -19,21 +18,43 @@ type ProgressBar struct { // NewProgressBar creates a new progress bar displaying the given progress // level. func NewProgressBar (progress float64) (element *ProgressBar) { + if progress < 0 { progress = 0 } + if progress > 1 { progress = 1 } element = &ProgressBar { progress: progress } + element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "progressBar") - element.Core, element.core = core.NewCore(element, element.draw) element.updateMinimumSize() return } +// Entity returns this element's entity. +func (element *ProgressBar) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *ProgressBar) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + + pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { }) + padding := element.theme.Padding(tomo.PatternSunken) + pattern.Draw(destination, bounds) + bounds = padding.Apply(bounds) + meterBounds := image.Rect ( + bounds.Min.X, bounds.Min.Y, + bounds.Min.X + int(float64(bounds.Dx()) * element.progress), + bounds.Max.Y) + mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { }) + mercury.Draw(destination, meterBounds) +} + // SetProgress sets the progress level of the bar. func (element *ProgressBar) SetProgress (progress float64) { + if progress < 0 { progress = 0 } + if progress > 1 { progress = 1 } if progress == element.progress { return } element.progress = progress - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. @@ -41,7 +62,7 @@ func (element *ProgressBar) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -49,35 +70,13 @@ func (element *ProgressBar) SetConfig (new tomo.Config) { if new == nil || new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } func (element *ProgressBar) updateMinimumSize() { padding := element.theme.Padding(tomo.PatternSunken) innerPadding := element.theme.Padding(tomo.PatternMercury) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( padding.Horizontal() + innerPadding.Horizontal(), padding.Vertical() + innerPadding.Vertical()) } - -func (element *ProgressBar) redo () { - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *ProgressBar) draw () { - bounds := element.Bounds() - - pattern := element.theme.Pattern(tomo.PatternSunken, tomo.State { }) - padding := element.theme.Padding(tomo.PatternSunken) - pattern.Draw(element.core, bounds) - bounds = padding.Apply(bounds) - meterBounds := image.Rect ( - bounds.Min.X, bounds.Min.Y, - bounds.Min.X + int(float64(bounds.Dx()) * element.progress), - bounds.Max.Y) - mercury := element.theme.Pattern(tomo.PatternMercury, tomo.State { }) - mercury.Draw(element.core, meterBounds) -} diff --git a/elements/scroll.go b/elements/scroll.go new file mode 100644 index 0000000..929743b --- /dev/null +++ b/elements/scroll.go @@ -0,0 +1,232 @@ +package elements + +import "image" +import "git.tebibyte.media/sashakoshka/tomo" +// import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" +import "git.tebibyte.media/sashakoshka/tomo/default/theme" +import "git.tebibyte.media/sashakoshka/tomo/default/config" + +type ScrollMode int; const ( + ScrollNeither ScrollMode = 0 + ScrollVertical = 1 + ScrollHorizontal = 2 + ScrollBoth = ScrollVertical | ScrollHorizontal +) + +// Includes returns whether a scroll mode has been or'd with another scroll +// mode. +func (mode ScrollMode) Includes (sub ScrollMode) bool { + return (mode & sub) > 0 +} + +type Scroll struct { + entity tomo.ContainerEntity + + child tomo.Scrollable + horizontal *ScrollBar + vertical *ScrollBar + + config config.Wrapped + theme theme.Wrapped +} + +func NewScroll (mode ScrollMode, child tomo.Scrollable) (element *Scroll) { + element = &Scroll { } + element.theme.Case = tomo.C("tomo", "scroll") + element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) + + if mode.Includes(ScrollHorizontal) { + element.horizontal = NewScrollBar(false) + element.horizontal.OnScroll (func (viewport image.Point) { + if element.child != nil { + element.child.ScrollTo(viewport) + } + if element.vertical != nil { + element.vertical.SetBounds ( + element.child.ScrollContentBounds(), + element.child.ScrollViewportBounds()) + } + }) + element.entity.Adopt(element.horizontal) + } + if mode.Includes(ScrollVertical) { + element.vertical = NewScrollBar(true) + element.vertical.OnScroll (func (viewport image.Point) { + if element.child != nil { + element.child.ScrollTo(viewport) + } + if element.horizontal != nil { + element.horizontal.SetBounds ( + element.child.ScrollContentBounds(), + element.child.ScrollViewportBounds()) + } + }) + element.entity.Adopt(element.vertical) + } + + element.Adopt(child) + return +} + +func (element *Scroll) Entity () tomo.Entity { + return element.entity +} + +func (element *Scroll) Draw (destination canvas.Canvas) { + if element.horizontal != nil && element.vertical != nil { + bounds := element.entity.Bounds() + bounds.Min = image.Pt ( + bounds.Max.X - element.vertical.Entity().Bounds().Dx(), + bounds.Max.Y - element.horizontal.Entity().Bounds().Dy()) + state := tomo.State { } + deadArea := element.theme.Pattern(tomo.PatternDead, state) + deadArea.Draw(canvas.Cut(destination, bounds), bounds) + } +} + +func (element *Scroll) Layout () { + bounds := element.entity.Bounds() + child := bounds + + iHorizontal := element.entity.IndexOf(element.horizontal) + iVertical := element.entity.IndexOf(element.vertical) + iChild := element.entity.IndexOf(element.child) + + var horizontal, vertical image.Rectangle + + if element.horizontal != nil { + _, hMinHeight := element.entity.ChildMinimumSize(iHorizontal) + child.Max.Y -= hMinHeight + } + if element.vertical != nil { + vMinWidth, _ := element.entity.ChildMinimumSize(iVertical) + child.Max.X -= vMinWidth + } + + horizontal.Min.X = bounds.Min.X + horizontal.Max.X = child.Max.X + horizontal.Min.Y = child.Max.Y + horizontal.Max.Y = bounds.Max.Y + + vertical.Min.X = child.Max.X + vertical.Max.X = bounds.Max.X + vertical.Min.Y = bounds.Min.Y + vertical.Max.Y = child.Max.Y + + if element.horizontal != nil { + element.entity.PlaceChild (iHorizontal, horizontal) + } + if element.vertical != nil { + element.entity.PlaceChild(iVertical, vertical) + } + if element.child != nil { + element.entity.PlaceChild(iChild, child) + } +} + +func (element *Scroll) DrawBackground (destination canvas.Canvas) { + element.entity.DrawBackground(destination) +} + +func (element *Scroll) Adopt (child tomo.Scrollable) { + if element.child != nil { + element.entity.Disown(element.entity.IndexOf(element.child)) + } + if child != nil { + element.entity.Adopt(child) + } + element.child = child + + element.updateEnabled() + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Scroll) HandleChildMinimumSizeChange (tomo.Element) { + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Scroll) HandleChildScrollBoundsChange (tomo.Scrollable) { + element.updateEnabled() + viewportBounds := element.child.ScrollViewportBounds() + contentBounds := element.child.ScrollContentBounds() + if element.horizontal != nil { + element.horizontal.SetBounds(contentBounds, viewportBounds) + } + if element.vertical != nil { + element.vertical.SetBounds(contentBounds, viewportBounds) + } +} + +func (element *Scroll) HandleScroll ( + x, y int, + deltaX, deltaY float64, +) { + horizontal, vertical := element.child.ScrollAxes() + if !horizontal { deltaX = 0 } + if !vertical { deltaY = 0 } + element.scrollChildBy(int(deltaX), int(deltaY)) +} + +func (element *Scroll) SetTheme (theme tomo.Theme) { + if theme == element.theme.Theme { return } + element.theme.Theme = theme + element.updateMinimumSize() + element.entity.Invalidate() + element.entity.InvalidateLayout() +} + +func (element *Scroll) SetConfig (config tomo.Config) { + element.config.Config = config +} + +func (element *Scroll) updateMinimumSize () { + var width, height int + + if element.child != nil { + width, height = element.entity.ChildMinimumSize ( + element.entity.IndexOf(element.child)) + } + if element.horizontal != nil { + hMinWidth, hMinHeight := element.entity.ChildMinimumSize ( + element.entity.IndexOf(element.horizontal)) + height += hMinHeight + if hMinWidth > width { + width = hMinWidth + } + } + if element.vertical != nil { + vMinWidth, vMinHeight := element.entity.ChildMinimumSize ( + element.entity.IndexOf(element.vertical)) + width += vMinWidth + if vMinHeight > height { + height = vMinHeight + } + } + element.entity.SetMinimumSize(width, height) +} + +func (element *Scroll) updateEnabled () { + horizontal, vertical := false, false + if element.child != nil { + horizontal, vertical = element.child.ScrollAxes() + } + if element.horizontal != nil { + element.horizontal.SetEnabled(horizontal) + } + if element.vertical != nil { + element.vertical.SetEnabled(vertical) + } +} + +func (element *Scroll) scrollChildBy (x, y int) { + if element.child == nil { return } + scrollPoint := + element.child.ScrollViewportBounds().Min. + Add(image.Pt(x, y)) + element.child.ScrollTo(scrollPoint) +} diff --git a/elements/scrollbar.go b/elements/scrollbar.go index 89b8262..f64731b 100644 --- a/elements/scrollbar.go +++ b/elements/scrollbar.go @@ -3,10 +3,17 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +// Orientation represents an orientation configuration that can be passed to +// scrollbars and sliders. +type Orientation bool; const ( + Vertical Orientation = true + Horizontal = false +) + // ScrollBar is an element similar to Slider, but it has special behavior that // makes it well suited for controlling the viewport position on one axis of a // scrollable element. Instead of having a value from zero to one, it stores @@ -19,8 +26,7 @@ import "git.tebibyte.media/sashakoshka/tomo/default/config" // Typically, you wont't want to use a ScrollBar by itself. A ScrollContainer is // better for most cases. type ScrollBar struct { - *core.Core - core core.CoreControl + entity tomo.ContainerEntity vertical bool enabled bool @@ -38,28 +44,42 @@ type ScrollBar struct { onScroll func (viewport image.Point) } -// NewScrollBar creates a new scroll bar. If vertical is set to true, the scroll -// bar will be vertical instead of horizontal. -func NewScrollBar (vertical bool) (element *ScrollBar) { +// NewScrollBar creates a new scroll bar. +func NewScrollBar (orientation Orientation) (element *ScrollBar) { element = &ScrollBar { - vertical: vertical, + vertical: bool(orientation), enabled: true, } - if vertical { + if orientation == Vertical { element.theme.Case = tomo.C("tomo", "scrollBarHorizontal") } else { element.theme.Case = tomo.C("tomo", "scrollBarVertical") } - element.Core, element.core = core.NewCore(element, element.handleResize) + element.entity = tomo.NewEntity(element).(tomo.ContainerEntity) element.updateMinimumSize() return } -func (element *ScrollBar) handleResize () { - if element.core.HasImage() { - element.recalculate() - element.draw() +// Entity returns this element's entity. +func (element *ScrollBar) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *ScrollBar) Draw (destination canvas.Canvas) { + element.recalculate() + + bounds := element.entity.Bounds() + state := tomo.State { + Disabled: !element.Enabled(), + Pressed: element.dragging, } + element.theme.Pattern(tomo.PatternGutter, state).Draw ( + destination, + bounds) + element.theme.Pattern(tomo.PatternHandle, state).Draw ( + destination, + element.bar) } func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) { @@ -69,10 +89,10 @@ func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) { if point.In(element.bar) { // the mouse is pressed down within the bar's handle element.dragging = true - element.drawAndPush() + element.entity.Invalidate() element.dragOffset = point.Sub(element.bar.Min). - Add(element.Bounds().Min) + Add(element.entity.Bounds().Min) element.dragTo(point) } else { // the mouse is pressed down within the bar's gutter @@ -112,7 +132,7 @@ func (element *ScrollBar) HandleMouseDown (x, y int, button input.Button) { func (element *ScrollBar) HandleMouseUp (x, y int, button input.Button) { if element.dragging { element.dragging = false - element.drawAndPush() + element.entity.Invalidate() } } @@ -134,7 +154,7 @@ func (element *ScrollBar) HandleScroll (x, y int, deltaX, deltaY float64) { func (element *ScrollBar) SetEnabled (enabled bool) { if element.enabled == enabled { return } element.enabled = enabled - element.drawAndPush() + element.entity.Invalidate() } // Enabled returns whether or not the element is enabled. @@ -146,8 +166,7 @@ func (element *ScrollBar) Enabled () (enabled bool) { func (element *ScrollBar) SetBounds (content, viewport image.Rectangle) { element.contentBounds = content element.viewportBounds = viewport - element.recalculate() - element.drawAndPush() + element.entity.Invalidate() } // OnScroll sets a function to be called when the user tries to move the scroll @@ -163,7 +182,7 @@ func (element *ScrollBar) OnScroll (callback func (viewport image.Point)) { func (element *ScrollBar) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new - element.drawAndPush() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -171,7 +190,7 @@ func (element *ScrollBar) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.drawAndPush() + element.entity.Invalidate() } func (element *ScrollBar) isAfterHandle (point image.Point) bool { @@ -184,10 +203,10 @@ func (element *ScrollBar) isAfterHandle (point image.Point) bool { func (element *ScrollBar) fallbackDragOffset () image.Point { if element.vertical { - return element.Bounds().Min. + return element.entity.Bounds().Min. Add(image.Pt(0, element.bar.Dy() / 2)) } else { - return element.Bounds().Min. + return element.entity.Bounds().Min. Add(image.Pt(element.bar.Dx() / 2, 0)) } } @@ -236,7 +255,7 @@ func (element *ScrollBar) recalculate () { } func (element *ScrollBar) recalculateVertical () { - bounds := element.Bounds() + bounds := element.entity.Bounds() padding := element.theme.Padding(tomo.PatternGutter) element.track = padding.Apply(bounds) @@ -263,7 +282,7 @@ func (element *ScrollBar) recalculateVertical () { } func (element *ScrollBar) recalculateHorizontal () { - bounds := element.Bounds() + bounds := element.entity.Bounds() padding := element.theme.Padding(tomo.PatternGutter) element.track = padding.Apply(bounds) @@ -293,33 +312,12 @@ func (element *ScrollBar) updateMinimumSize () { gutterPadding := element.theme.Padding(tomo.PatternGutter) handlePadding := element.theme.Padding(tomo.PatternHandle) if element.vertical { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal(), gutterPadding.Vertical() + handlePadding.Vertical() * 2) } else { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal() * 2, gutterPadding.Vertical() + handlePadding.Vertical()) } } - -func (element *ScrollBar) drawAndPush () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *ScrollBar) draw () { - bounds := element.Bounds() - state := tomo.State { - Disabled: !element.Enabled(), - Pressed: element.dragging, - } - element.theme.Pattern(tomo.PatternGutter, state).Draw ( - element.core, - bounds) - element.theme.Pattern(tomo.PatternHandle, state).Draw ( - element.core, - element.bar) -} diff --git a/elements/slider.go b/elements/slider.go index e7d38b8..f14903b 100644 --- a/elements/slider.go +++ b/elements/slider.go @@ -3,23 +3,21 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Slider is a slider control with a floating point value between zero and one. type Slider struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl - - value float64 - vertical bool - dragging bool + entity tomo.FocusableEntity + + value float64 + vertical bool + dragging bool + enabled bool dragOffset int - track image.Rectangle - bar image.Rectangle + track image.Rectangle + bar image.Rectangle config config.Wrapped theme theme.Wrapped @@ -28,25 +26,77 @@ type Slider struct { onRelease func () } -// NewSlider creates a new slider with the specified value. If vertical is set -// to true, -func NewSlider (value float64, vertical bool) (element *Slider) { +// NewSlider creates a new slider with the specified value. +func NewSlider (value float64, orientation Orientation) (element *Slider) { element = &Slider { value: value, - vertical: vertical, + vertical: bool(orientation), } - if vertical { + if orientation == Vertical { element.theme.Case = tomo.C("tomo", "sliderVertical") } else { element.theme.Case = tomo.C("tomo", "sliderHorizontal") } - element.Core, element.core = core.NewCore(element, element.draw) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.redo) + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.updateMinimumSize() return } +// Entity returns this element's entity. +func (element *Slider) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Slider) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + element.track = element.theme.Padding(tomo.PatternGutter).Apply(bounds) + if element.vertical { + barSize := element.track.Dx() + element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) + barOffset := + float64(element.track.Dy() - barSize) * + (1 - element.value) + element.bar = element.bar.Add(image.Pt(0, int(barOffset))) + } else { + barSize := element.track.Dy() + element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) + barOffset := + float64(element.track.Dx() - barSize) * + element.value + element.bar = element.bar.Add(image.Pt(int(barOffset), 0)) + } + + state := tomo.State { + Disabled: !element.Enabled(), + Focused: element.entity.Focused(), + Pressed: element.dragging, + } + element.theme.Pattern(tomo.PatternGutter, state).Draw(destination, bounds) + element.theme.Pattern(tomo.PatternHandle, state).Draw(destination, bounds) +} + +// Focus gives this element input focus. +func (element *Slider) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this slider can be dragged or not. +func (element *Slider) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this slider can be dragged or not. +func (element *Slider) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + +func (element *Slider) HandleFocusChange () { + element.entity.Invalidate() +} + func (element *Slider) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } element.Focus() @@ -56,7 +106,7 @@ func (element *Slider) HandleMouseDown (x, y int, button input.Button) { if element.onSlide != nil { element.onSlide() } - element.redo() + element.entity.Invalidate() } } @@ -66,7 +116,7 @@ func (element *Slider) HandleMouseUp (x, y int, button input.Button) { if element.onRelease != nil { element.onRelease() } - element.redo() + element.entity.Invalidate() } func (element *Slider) HandleMotion (x, y int) { @@ -76,7 +126,7 @@ func (element *Slider) HandleMotion (x, y int) { if element.onSlide != nil { element.onSlide() } - element.redo() + element.entity.Invalidate() } } @@ -110,11 +160,6 @@ func (element *Slider) Value () (value float64) { return element.value } -// SetEnabled sets whether or not the slider can be interacted with. -func (element *Slider) SetEnabled (enabled bool) { - element.focusableControl.SetEnabled(enabled) -} - // SetValue sets the slider's value. func (element *Slider) SetValue (value float64) { if value < 0 { value = 0 } @@ -126,7 +171,7 @@ func (element *Slider) SetValue (value float64) { if element.onRelease != nil { element.onRelease() } - element.redo() + element.entity.Invalidate() } // OnSlide sets a function to be called every time the slider handle changes @@ -144,7 +189,7 @@ func (element *Slider) OnRelease (callback func ()) { func (element *Slider) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -152,7 +197,7 @@ func (element *Slider) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } func (element *Slider) changeValue (delta float64) { @@ -166,7 +211,7 @@ func (element *Slider) changeValue (delta float64) { if element.onRelease != nil { element.onRelease() } - element.redo() + element.entity.Invalidate() } func (element *Slider) valueFor (x, y int) (value float64) { @@ -190,51 +235,12 @@ func (element *Slider) updateMinimumSize () { gutterPadding := element.theme.Padding(tomo.PatternGutter) handlePadding := element.theme.Padding(tomo.PatternHandle) if element.vertical { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal(), gutterPadding.Vertical() + handlePadding.Vertical() * 2) } else { - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( gutterPadding.Horizontal() + handlePadding.Horizontal() * 2, gutterPadding.Vertical() + handlePadding.Vertical()) } } - -func (element *Slider) redo () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *Slider) draw () { - bounds := element.Bounds() - element.track = element.theme.Padding(tomo.PatternGutter).Apply(bounds) - if element.vertical { - barSize := element.track.Dx() - element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) - barOffset := - float64(element.track.Dy() - barSize) * - (1 - element.value) - element.bar = element.bar.Add(image.Pt(0, int(barOffset))) - } else { - barSize := element.track.Dy() - element.bar = image.Rect(0, 0, barSize, barSize).Add(bounds.Min) - barOffset := - float64(element.track.Dx() - barSize) * - element.value - element.bar = element.bar.Add(image.Pt(int(barOffset), 0)) - } - - state := tomo.State { - Focused: element.Focused(), - Disabled: !element.Enabled(), - Pressed: element.dragging, - } - element.theme.Pattern(tomo.PatternGutter, state).Draw ( - element.core, - bounds) - element.theme.Pattern(tomo.PatternHandle, state).Draw ( - element.core, - element.bar) -} diff --git a/elements/spacer.go b/elements/spacer.go index f1655d0..9b0ff4d 100644 --- a/elements/spacer.go +++ b/elements/spacer.go @@ -1,86 +1,87 @@ package elements import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Spacer can be used to put space between two elements.. type Spacer struct { - *core.Core - core core.CoreControl + entity tomo.Entity + line bool config config.Wrapped theme theme.Wrapped } -// NewSpacer creates a new spacer. If line is set to true, the spacer will be -// filled with a line color, and if compressed to its minimum width or height, -// will appear as a line. -func NewSpacer (line bool) (element *Spacer) { - element = &Spacer { line: line } +// NewSpacer creates a new spacer. +func NewSpacer () (element *Spacer) { + element = &Spacer { } + element.entity = tomo.NewEntity(element) element.theme.Case = tomo.C("tomo", "spacer") - element.Core, element.core = core.NewCore(element, element.draw) element.updateMinimumSize() return } +// NewLine creates a new line separator. +func NewLine () (element *Spacer) { + element = NewSpacer() + element.SetLine(true) + return +} + +// Entity returns this element's entity. +func (element *Spacer) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *Spacer) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + + if element.line { + pattern := element.theme.Pattern ( + tomo.PatternLine, + tomo.State { }) + pattern.Draw(destination, bounds) + } else { + pattern := element.theme.Pattern ( + tomo.PatternBackground, + tomo.State { }) + pattern.Draw(destination, bounds) + } +} + /// SetLine sets whether or not the spacer will appear as a colored line. func (element *Spacer) SetLine (line bool) { if element.line == line { return } element.line = line element.updateMinimumSize() - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } + element.entity.Invalidate() } // SetTheme sets the element's theme. func (element *Spacer) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } element.theme.Theme = new - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. func (element *Spacer) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new - element.redo() + element.entity.Invalidate() } func (element *Spacer) updateMinimumSize () { if element.line { padding := element.theme.Padding(tomo.PatternLine) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( padding.Horizontal(), padding.Vertical()) } else { - element.core.SetMinimumSize(1, 1) - } -} - -func (element *Spacer) redo () { - if !element.core.HasImage() { - element.draw() - element.core.DamageAll() - } -} - -func (element *Spacer) draw () { - bounds := element.Bounds() - - if element.line { - pattern := element.theme.Pattern ( - tomo.PatternLine, - tomo.State { }) - pattern.Draw(element.core, bounds) - } else { - pattern := element.theme.Pattern ( - tomo.PatternBackground, - tomo.State { }) - pattern.Draw(element.core, bounds) + element.entity.SetMinimumSize(1, 1) } } diff --git a/elements/switch.go b/elements/switch.go index 77bbc84..70516eb 100644 --- a/elements/switch.go +++ b/elements/switch.go @@ -3,20 +3,18 @@ package elements import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Switch is a toggle-able on/off switch with an optional label. It is // functionally identical to Checkbox, but plays a different semantic role. type Switch struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity tomo.FocusableEntity drawer textdraw.Drawer + enabled bool pressed bool checked bool text string @@ -31,12 +29,11 @@ type Switch struct { func NewSwitch (text string, on bool) (element *Switch) { element = &Switch { checked: on, - text: text, + text: text, + enabled: true, } + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) element.theme.Case = tomo.C("tomo", "switch") - element.Core, element.core = core.NewCore(element, element.draw) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.redo) element.drawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, tomo.FontSizeNormal)) @@ -45,129 +42,25 @@ func NewSwitch (text string, on bool) (element *Switch) { return } -func (element *Switch) HandleMouseDown (x, y int, button input.Button) { - if !element.Enabled() { return } - element.Focus() - element.pressed = true - element.redo() +// Entity returns this element's entity. +func (element *Switch) Entity () tomo.Entity { + return element.entity } -func (element *Switch) HandleMouseUp (x, y int, button input.Button) { - if button != input.ButtonLeft || !element.pressed { return } - - element.pressed = false - within := image.Point { x, y }. - In(element.Bounds()) - if within { - element.checked = !element.checked - } - - if element.core.HasImage() { - element.draw() - element.core.DamageAll() - } - if within && element.onToggle != nil { - element.onToggle() - } -} - -func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter { - element.pressed = true - element.redo() - } -} - -func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) { - if key == input.KeyEnter && element.pressed { - element.pressed = false - element.checked = !element.checked - element.redo() - if element.onToggle != nil { - element.onToggle() - } - } -} - -// OnToggle sets the function to be called when the switch is flipped. -func (element *Switch) OnToggle (callback func ()) { - element.onToggle = callback -} - -// Value reports whether or not the switch is currently on. -func (element *Switch) Value () (on bool) { - return element.checked -} - -// SetEnabled sets whether this switch can be flipped or not. -func (element *Switch) SetEnabled (enabled bool) { - element.focusableControl.SetEnabled(enabled) -} - -// SetText sets the checkbox's label text. -func (element *Switch) SetText (text string) { - if element.text == text { return } - - element.text = text - element.drawer.SetText([]rune(text)) - element.updateMinimumSize() - element.redo() -} - -// SetTheme sets the element's theme. -func (element *Switch) SetTheme (new tomo.Theme) { - if new == element.theme.Theme { return } - element.theme.Theme = new - element.drawer.SetFace (element.theme.FontFace ( - tomo.FontStyleRegular, - tomo.FontSizeNormal)) - element.updateMinimumSize() - element.redo() -} - -// SetConfig sets the element's configuration. -func (element *Switch) SetConfig (new tomo.Config) { - if new == element.config.Config { return } - element.config.Config = new - element.updateMinimumSize() - element.redo() -} - -func (element *Switch) redo () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } -} - -func (element *Switch) updateMinimumSize () { - textBounds := element.drawer.LayoutBounds() - lineHeight := element.drawer.LineHeight().Round() - - if element.text == "" { - element.core.SetMinimumSize(lineHeight * 2, lineHeight) - } else { - element.core.SetMinimumSize ( - lineHeight * 2 + - element.theme.Margin(tomo.PatternBackground).X + - textBounds.Dx(), - lineHeight) - } -} - -func (element *Switch) draw () { - bounds := element.Bounds() +// Draw causes the element to draw to the specified destination canvas. +func (element *Switch) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() handleBounds := image.Rect(0, 0, bounds.Dy(), bounds.Dy()).Add(bounds.Min) gutterBounds := image.Rect(0, 0, bounds.Dy() * 2, bounds.Dy()).Add(bounds.Min) state := tomo.State { Disabled: !element.Enabled(), - Focused: element.Focused(), + Focused: element.entity.Focused(), Pressed: element.pressed, + On: element.checked, } - element.core.DrawBackground ( - element.theme.Pattern(tomo.PatternBackground, state)) + element.entity.DrawBackground(destination) if element.checked { handleBounds.Min.X += bounds.Dy() @@ -185,11 +78,11 @@ func (element *Switch) draw () { gutterPattern := element.theme.Pattern ( tomo.PatternGutter, state) - gutterPattern.Draw(element.core, gutterBounds) + gutterPattern.Draw(destination, gutterBounds) handlePattern := element.theme.Pattern ( tomo.PatternHandle, state) - handlePattern.Draw(element.core, handleBounds) + handlePattern.Draw(destination, handleBounds) textBounds := element.drawer.LayoutBounds() offset := bounds.Min.Add(image.Point { @@ -201,5 +94,121 @@ func (element *Switch) draw () { offset.X -= textBounds.Min.X foreground := element.theme.Color(tomo.ColorForeground, state) - element.drawer.Draw(element.core, foreground, offset) + element.drawer.Draw(destination, foreground, offset) +} + +func (element *Switch) HandleFocusChange () { + element.entity.Invalidate() +} + +func (element *Switch) HandleMouseDown (x, y int, button input.Button) { + if !element.Enabled() { return } + element.Focus() + element.pressed = true + element.entity.Invalidate() +} + +func (element *Switch) HandleMouseUp (x, y int, button input.Button) { + if button != input.ButtonLeft || !element.pressed { return } + + element.pressed = false + within := image.Point { x, y }. + In(element.entity.Bounds()) + if within { + element.checked = !element.checked + } + + element.entity.Invalidate() + if within && element.onToggle != nil { + element.onToggle() + } +} + +func (element *Switch) HandleKeyDown (key input.Key, modifiers input.Modifiers) { + if key == input.KeyEnter { + element.pressed = true + element.entity.Invalidate() + } +} + +func (element *Switch) HandleKeyUp (key input.Key, modifiers input.Modifiers) { + if key == input.KeyEnter && element.pressed { + element.pressed = false + element.checked = !element.checked + element.entity.Invalidate() + if element.onToggle != nil { + element.onToggle() + } + } +} + +// OnToggle sets the function to be called when the switch is flipped. +func (element *Switch) OnToggle (callback func ()) { + element.onToggle = callback +} + +// Value reports whether or not the switch is currently on. +func (element *Switch) Value () (on bool) { + return element.checked +} + +// Focus gives this element input focus. +func (element *Switch) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this switch is enabled or not. +func (element *Switch) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this switch can be toggled or not. +func (element *Switch) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + +// SetText sets the checkbox's label text. +func (element *Switch) SetText (text string) { + if element.text == text { return } + + element.text = text + element.drawer.SetText([]rune(text)) + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetTheme sets the element's theme. +func (element *Switch) SetTheme (new tomo.Theme) { + if new == element.theme.Theme { return } + element.theme.Theme = new + element.drawer.SetFace (element.theme.FontFace ( + tomo.FontStyleRegular, + tomo.FontSizeNormal)) + element.updateMinimumSize() + element.entity.Invalidate() +} + +// SetConfig sets the element's configuration. +func (element *Switch) SetConfig (new tomo.Config) { + if new == element.config.Config { return } + element.config.Config = new + element.updateMinimumSize() + element.entity.Invalidate() +} + +func (element *Switch) updateMinimumSize () { + textBounds := element.drawer.LayoutBounds() + lineHeight := element.drawer.LineHeight().Round() + + if element.text == "" { + element.entity.SetMinimumSize(lineHeight * 2, lineHeight) + } else { + element.entity.SetMinimumSize ( + lineHeight * 2 + + element.theme.Margin(tomo.PatternBackground).X + + textBounds.Dx(), + lineHeight) + } } diff --git a/elements/testing/artist.go b/elements/testing/artist.go index 15a26e9..a7546a2 100644 --- a/elements/testing/artist.go +++ b/elements/testing/artist.go @@ -4,11 +4,11 @@ import "fmt" import "time" import "image" import "image/color" +import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/shatter" import "git.tebibyte.media/sashakoshka/tomo/textdraw" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" import "git.tebibyte.media/sashakoshka/tomo/artist/patterns" import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font" @@ -16,49 +16,52 @@ import defaultfont "git.tebibyte.media/sashakoshka/tomo/default/font" // Artist is an element that displays shapes and patterns drawn by the artist // package in order to test it. type Artist struct { - *core.Core - core core.CoreControl + entity tomo.Entity } // NewArtist creates a new artist test element. func NewArtist () (element *Artist) { element = &Artist { } - element.Core, element.core = core.NewCore(element, element.draw) - element.core.SetMinimumSize(240, 240) + element.entity = tomo.NewEntity(element) + element.entity.SetMinimumSize(240, 240) return } -func (element *Artist) draw () { - bounds := element.Bounds() - patterns.Uhex(0x000000FF).Draw(element.core, bounds) +func (element *Artist) Entity () tomo.Entity { + return element.entity +} + +func (element *Artist) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + patterns.Uhex(0x000000FF).Draw(destination, bounds) drawStart := time.Now() // 0, 0 - 3, 0 for x := 0; x < 4; x ++ { - element.colorLines(x + 1, element.cellAt(x, 0).Bounds()) + element.colorLines(destination, x + 1, element.cellAt(destination, x, 0).Bounds()) } // 4, 0 - c40 := element.cellAt(4, 0) + c40 := element.cellAt(destination, 4, 0) shapes.StrokeColorRectangle(c40, artist.Hex(0x888888FF), c40.Bounds(), 1) shapes.ColorLine ( c40, artist.Hex(0xFF0000FF), 1, c40.Bounds().Min, c40.Bounds().Max) // 0, 1 - c01 := element.cellAt(0, 1) + c01 := element.cellAt(destination, 0, 1) shapes.StrokeColorRectangle(c01, artist.Hex(0x888888FF), c01.Bounds(), 1) - shapes.FillColorEllipse(element.core, artist.Hex(0x00FF00FF), c01.Bounds()) + shapes.FillColorEllipse(destination, artist.Hex(0x00FF00FF), c01.Bounds()) // 1, 1 - 3, 1 for x := 1; x < 4; x ++ { - c := element.cellAt(x, 1) + c := element.cellAt(destination, x, 1) shapes.StrokeColorRectangle ( - element.core, artist.Hex(0x888888FF), + destination, artist.Hex(0x888888FF), c.Bounds(), 1) shapes.StrokeColorEllipse ( - element.core, + destination, []color.RGBA { artist.Hex(0xFF0000FF), artist.Hex(0x00FF00FF), @@ -68,7 +71,7 @@ func (element *Artist) draw () { } // 4, 1 - c41 := element.cellAt(4, 1) + c41 := element.cellAt(destination, 4, 1) shatterPos := c41.Bounds().Min rocks := []image.Rectangle { image.Rect(3, 12, 13, 23).Add(shatterPos), @@ -85,46 +88,46 @@ func (element *Artist) draw () { patterns.Uhex(0xFF00FFFF), patterns.Uhex(0xFFFF00FF), patterns.Uhex(0x00FFFFFF), - } [index % 5].Draw(element.core, tile) + } [index % 5].Draw(destination, tile) } // 0, 2 - c02 := element.cellAt(0, 2) + c02 := element.cellAt(destination, 0, 2) shapes.StrokeColorRectangle(c02, artist.Hex(0x888888FF), c02.Bounds(), 1) shapes.FillEllipse(c02, c41, c02.Bounds()) // 1, 2 - c12 := element.cellAt(1, 2) + c12 := element.cellAt(destination, 1, 2) shapes.StrokeColorRectangle(c12, artist.Hex(0x888888FF), c12.Bounds(), 1) shapes.StrokeEllipse(c12, c41, c12.Bounds(), 5) // 2, 2 - c22 := element.cellAt(2, 2) + c22 := element.cellAt(destination, 2, 2) shapes.FillRectangle(c22, c41, c22.Bounds()) // 3, 2 - c32 := element.cellAt(3, 2) + c32 := element.cellAt(destination, 3, 2) shapes.StrokeRectangle(c32, c41, c32.Bounds(), 5) // 4, 2 - c42 := element.cellAt(4, 2) + c42 := element.cellAt(destination, 4, 2) // 0, 3 - c03 := element.cellAt(0, 3) + c03 := element.cellAt(destination, 0, 3) patterns.Border { Canvas: element.thingy(c42), Inset: artist.Inset { 8, 8, 8, 8 }, }.Draw(c03, c03.Bounds()) // 1, 3 - c13 := element.cellAt(1, 3) + c13 := element.cellAt(destination, 1, 3) patterns.Border { Canvas: element.thingy(c42), Inset: artist.Inset { 8, 8, 8, 8 }, }.Draw(c13, c13.Bounds().Inset(10)) // 2, 3 - c23 := element.cellAt(2, 3) + c23 := element.cellAt(destination, 2, 3) patterns.Border { Canvas: element.thingy(c42), Inset: artist.Inset { 8, 8, 8, 8 }, @@ -143,51 +146,51 @@ func (element *Artist) draw () { drawTime.Milliseconds(), drawTime.Microseconds()))) textDrawer.Draw ( - element.core, artist.Hex(0xFFFFFFFF), + destination, artist.Hex(0xFFFFFFFF), image.Pt(bounds.Min.X + 8, bounds.Max.Y - 24)) } -func (element *Artist) colorLines (weight int, bounds image.Rectangle) { +func (element *Artist) colorLines (destination canvas.Canvas, weight int, bounds image.Rectangle) { bounds = bounds.Inset(4) c := artist.Hex(0xFFFFFFFF) - shapes.ColorLine(element.core, c, weight, bounds.Min, bounds.Max) + shapes.ColorLine(destination, c, weight, bounds.Min, bounds.Max) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Max.X, bounds.Min.Y), image.Pt(bounds.Min.X, bounds.Max.Y)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Max.X, bounds.Min.Y + 16), image.Pt(bounds.Min.X, bounds.Max.Y - 16)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Min.X, bounds.Min.Y + 16), image.Pt(bounds.Max.X, bounds.Max.Y - 16)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Min.X + 20, bounds.Min.Y), image.Pt(bounds.Max.X - 20, bounds.Max.Y)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Max.X - 20, bounds.Min.Y), image.Pt(bounds.Min.X + 20, bounds.Max.Y)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Min.X, bounds.Min.Y + bounds.Dy() / 2), image.Pt(bounds.Max.X, bounds.Min.Y + bounds.Dy() / 2)) shapes.ColorLine ( - element.core, c, weight, + destination, c, weight, image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Min.Y), image.Pt(bounds.Min.X + bounds.Dx() / 2, bounds.Max.Y)) } -func (element *Artist) cellAt (x, y int) (canvas.Canvas) { - bounds := element.Bounds() +func (element *Artist) cellAt (destination canvas.Canvas, x, y int) (canvas.Canvas) { + bounds := element.entity.Bounds() cellBounds := image.Rectangle { } cellBounds.Min = bounds.Min cellBounds.Max.X = bounds.Min.X + bounds.Dx() / 5 cellBounds.Max.Y = bounds.Min.Y + (bounds.Dy() - 48) / 4 - return canvas.Cut (element.core, cellBounds.Add (image.Pt ( + return canvas.Cut (destination, cellBounds.Add (image.Pt ( x * cellBounds.Dx(), y * cellBounds.Dy()))) } diff --git a/elements/testing/mouse.go b/elements/testing/mouse.go index f2ee908..fa22b9a 100644 --- a/elements/testing/mouse.go +++ b/elements/testing/mouse.go @@ -4,17 +4,16 @@ import "image" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" import "git.tebibyte.media/sashakoshka/tomo/artist" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" // Mouse is an element capable of testing mouse input. When the mouse is clicked // and dragged on it, it draws a trail. type Mouse struct { - *core.Core - core core.CoreControl - drawing bool + entity tomo.Entity + pressed bool lastMousePos image.Point config config.Wrapped @@ -24,69 +23,63 @@ type Mouse struct { // NewMouse creates a new mouse test element. func NewMouse () (element *Mouse) { element = &Mouse { } - element.theme.Case = tomo.C("tomo", "piano") - element.Core, element.core = core.NewCore(element, element.draw) - element.core.SetMinimumSize(32, 32) + element.theme.Case = tomo.C("tomo", "mouse") + element.entity = tomo.NewEntity(element) + element.entity.SetMinimumSize(32, 32) return } +func (element *Mouse) Entity () tomo.Entity { + return element.entity +} + +func (element *Mouse) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + accent := element.theme.Color ( + tomo.ColorAccent, + tomo.State { }) + shapes.FillColorRectangle(destination, accent, bounds) + shapes.StrokeColorRectangle ( + destination, + artist.Hex(0x000000FF), + bounds, 1) + shapes.ColorLine ( + destination, artist.Hex(0xFFFFFFFF), 1, + bounds.Min.Add(image.Pt(1, 1)), + bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2))) + shapes.ColorLine ( + destination, artist.Hex(0xFFFFFFFF), 1, + bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)), + bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1))) + if element.pressed { + shapes.ColorLine ( + destination, artist.Hex(0x000000FF), 1, + bounds.Min, element.lastMousePos) + } +} + // SetTheme sets the element's theme. func (element *Mouse) SetTheme (new tomo.Theme) { element.theme.Theme = new - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. func (element *Mouse) SetConfig (new tomo.Config) { element.config.Config = new - element.redo() -} - -func (element *Mouse) redo () { - if !element.core.HasImage() { return } - element.draw() - element.core.DamageAll() -} - -func (element *Mouse) draw () { - bounds := element.Bounds() - accent := element.theme.Color ( - tomo.ColorAccent, - tomo.State { }) - shapes.FillColorRectangle(element.core, accent, bounds) - shapes.StrokeColorRectangle ( - element.core, - artist.Hex(0x000000FF), - bounds, 1) - shapes.ColorLine ( - element.core, artist.Hex(0xFFFFFFFF), 1, - bounds.Min.Add(image.Pt(1, 1)), - bounds.Min.Add(image.Pt(bounds.Dx() - 2, bounds.Dy() - 2))) - shapes.ColorLine ( - element.core, artist.Hex(0xFFFFFFFF), 1, - bounds.Min.Add(image.Pt(1, bounds.Dy() - 2)), - bounds.Min.Add(image.Pt(bounds.Dx() - 2, 1))) + element.entity.Invalidate() } func (element *Mouse) HandleMouseDown (x, y int, button input.Button) { - element.drawing = true - element.lastMousePos = image.Pt(x, y) + element.pressed = true } func (element *Mouse) HandleMouseUp (x, y int, button input.Button) { - element.drawing = false - mousePos := image.Pt(x, y) - element.core.DamageRegion (shapes.ColorLine ( - element.core, artist.Hex(0x000000FF), 1, - element.lastMousePos, mousePos)) - element.lastMousePos = mousePos + element.pressed = false } func (element *Mouse) HandleMotion (x, y int) { - if !element.drawing { return } - mousePos := image.Pt(x, y) - element.core.DamageRegion (shapes.ColorLine ( - element.core, artist.Hex(0x000000FF), 1, - element.lastMousePos, mousePos)) - element.lastMousePos = mousePos + if !element.pressed { return } + element.lastMousePos = image.Pt(x, y) + element.entity.Invalidate() } diff --git a/elements/textbox.go b/elements/textbox.go index 1903686..5f1ce2f 100644 --- a/elements/textbox.go +++ b/elements/textbox.go @@ -12,17 +12,20 @@ import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/textmanip" import "git.tebibyte.media/sashakoshka/tomo/fixedutil" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/theme" import "git.tebibyte.media/sashakoshka/tomo/default/config" +type textBoxEntity interface { + tomo.FocusableEntity + tomo.ScrollableEntity + tomo.LayoutEntity +} + // TextBox is a single-line text input. type TextBox struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl - + entity textBoxEntity + + enabled bool lastClick time.Time dragging int dot textmanip.Dot @@ -46,16 +49,9 @@ type TextBox struct { // a value. When the value is empty, the placeholder will be displayed in gray // text. func NewTextBox (placeholder, value string) (element *TextBox) { - element = &TextBox { } + element = &TextBox { enabled: true } element.theme.Case = tomo.C("tomo", "textBox") - element.Core, element.core = core.NewCore(element, element.handleResize) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore (element.core, func () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() - } - }) + element.entity = tomo.NewEntity(element).(textBoxEntity) element.placeholder = placeholder element.placeholderDrawer.SetFace (element.theme.FontFace ( tomo.FontStyleRegular, @@ -69,17 +65,87 @@ func NewTextBox (placeholder, value string) (element *TextBox) { return } -func (element *TextBox) handleResize () { - element.scrollToCursor() - element.draw() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) +// Entity returns this element's entity. +func (element *TextBox) Entity () tomo.Entity { + return element.entity +} + +// Draw causes the element to draw to the specified destination canvas. +func (element *TextBox) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + + state := element.state() + pattern := element.theme.Pattern(tomo.PatternInput, state) + padding := element.theme.Padding(tomo.PatternInput) + innerCanvas := canvas.Cut(destination, padding.Apply(bounds)) + pattern.Draw(destination, bounds) + offset := element.textOffset() + + if element.entity.Focused() && !element.dot.Empty() { + // draw selection bounds + accent := element.theme.Color(tomo.ColorAccent, state) + canon := element.dot.Canon() + foff := fixedutil.Pt(offset) + start := element.valueDrawer.PositionAt(canon.Start).Add(foff) + end := element.valueDrawer.PositionAt(canon.End).Add(foff) + end.Y += element.valueDrawer.LineHeight() + shapes.FillColorRectangle ( + innerCanvas, + accent, + image.Rectangle { + fixedutil.RoundPt(start), + fixedutil.RoundPt(end), + }) } + + if len(element.text) == 0 { + // draw placeholder + textBounds := element.placeholderDrawer.LayoutBounds() + foreground := element.theme.Color ( + tomo.ColorForeground, + tomo.State { Disabled: true }) + element.placeholderDrawer.Draw ( + innerCanvas, + foreground, + offset.Sub(textBounds.Min)) + } else { + // draw input value + textBounds := element.valueDrawer.LayoutBounds() + foreground := element.theme.Color(tomo.ColorForeground, state) + element.valueDrawer.Draw ( + innerCanvas, + foreground, + offset.Sub(textBounds.Min)) + } + + if element.entity.Focused() && element.dot.Empty() { + // draw cursor + foreground := element.theme.Color(tomo.ColorForeground, state) + cursorPosition := fixedutil.RoundPt ( + element.valueDrawer.PositionAt(element.dot.End)) + shapes.ColorLine ( + innerCanvas, + foreground, 1, + cursorPosition.Add(offset), + image.Pt ( + cursorPosition.X, + cursorPosition.Y + element.valueDrawer. + LineHeight().Round()).Add(offset)) + } +} + +// Layout causes the element to perform a layout operation. +func (element *TextBox) Layout () { + element.scrollToCursor() +} + +func (element *TextBox) HandleFocusChange () { + element.entity.Invalidate() } func (element *TextBox) HandleMouseDown (x, y int, button input.Button) { if !element.Enabled() { return } - if !element.Focused() { element.Focus() } + element.Focus() if button == input.ButtonLeft { runeIndex := element.atPosition(image.Pt(x, y)) @@ -94,7 +160,7 @@ func (element *TextBox) HandleMouseDown (x, y int, button input.Button) { element.lastClick = time.Now() } - element.redo() + element.entity.Invalidate() } } @@ -106,7 +172,7 @@ func (element *TextBox) HandleMotion (x, y int) { runeIndex := element.atPosition(image.Pt(x, y)) if runeIndex > -1 { element.dot.End = runeIndex - element.redo() + element.entity.Invalidate() } case 2: @@ -125,14 +191,14 @@ func (element *TextBox) HandleMotion (x, y int) { element.text, runeIndex) } - element.redo() + element.entity.Invalidate() } } } func (element *TextBox) textOffset () image.Point { padding := element.theme.Padding(tomo.PatternInput) - bounds := element.Bounds() + bounds := element.entity.Bounds() innerBounds := padding.Apply(bounds) textHeight := element.valueDrawer.LineHeight().Round() return bounds.Min.Add (image.Pt ( @@ -227,7 +293,7 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) element.clipboardPut(element.dot.Slice(element.text)) case key == 'v' && modifiers.Control: - window := element.core.Window() + window := element.entity.Window() if window == nil { break } window.Paste (func (d data.Data, err error) { if err != nil { return } @@ -262,25 +328,17 @@ func (element *TextBox) HandleKeyDown(key input.Key, modifiers input.Modifiers) } if (textChanged || scrollMemory != element.scroll) { - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } + element.entity.NotifyScrollBoundsChange() } if altered { - element.redo() - } -} - -func (element *TextBox) clipboardPut (text []rune) { - window := element.core.Window() - if window != nil { - window.Copy(data.Bytes(data.MimePlain, []byte(string(text)))) + element.entity.Invalidate() } } func (element *TextBox) HandleKeyUp(key input.Key, modifiers input.Modifiers) { } +// SetPlaceholder sets the element's placeholder text. func (element *TextBox) SetPlaceholder (placeholder string) { if element.placeholder == placeholder { return } @@ -288,9 +346,10 @@ func (element *TextBox) SetPlaceholder (placeholder string) { element.placeholderDrawer.SetText([]rune(placeholder)) element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } +// SetValue sets the input's value. func (element *TextBox) SetValue (text string) { // if element.text == text { return } @@ -301,27 +360,35 @@ func (element *TextBox) SetValue (text string) { element.dot = textmanip.EmptyDot(element.valueDrawer.Length()) } element.scrollToCursor() - element.redo() + element.entity.Invalidate() } +// Value returns the input's value. func (element *TextBox) Value () (value string) { return string(element.text) } +// Filled returns whether or not this element has a value. func (element *TextBox) Filled () (filled bool) { return len(element.text) > 0 } +// OnKeyDown specifies a function to be called when a key is pressed within the +// text input. func (element *TextBox) OnKeyDown ( callback func (key input.Key, modifiers input.Modifiers) (handled bool), ) { element.onKeyDown = callback } +// OnEnter specifies a function to be called when the enter key is pressed +// within this input. func (element *TextBox) OnEnter (callback func ()) { element.onEnter = callback } +// OnChange specifies a function to be called when the value of this input +// changes. func (element *TextBox) OnChange (callback func ()) { element.onChange = callback } @@ -332,6 +399,23 @@ func (element *TextBox) OnScrollBoundsChange (callback func ()) { element.onScrollBoundsChange = callback } +// Focus gives this element input focus. +func (element *TextBox) Focus () { + if !element.entity.Focused() { element.entity.Focus() } +} + +// Enabled returns whether this label can be edited or not. +func (element *TextBox) Enabled () bool { + return element.enabled +} + +// SetEnabled sets whether this label can be edited or not. +func (element *TextBox) SetEnabled (enabled bool) { + if element.enabled == enabled { return } + element.enabled = enabled + element.entity.Invalidate() +} + // ScrollContentBounds returns the full content size of the element. func (element *TextBox) ScrollContentBounds () (bounds image.Rectangle) { bounds = element.valueDrawer.LayoutBounds() @@ -348,11 +432,6 @@ func (element *TextBox) ScrollViewportBounds () (bounds image.Rectangle) { 0) } -func (element *TextBox) scrollViewportWidth () (width int) { - padding := element.theme.Padding(tomo.PatternInput) - return padding.Apply(element.Bounds()).Dx() -} - // ScrollTo scrolls the viewport to the specified point relative to // ScrollBounds. func (element *TextBox) ScrollTo (position image.Point) { @@ -365,10 +444,8 @@ func (element *TextBox) ScrollTo (position image.Point) { maxPosition := contentBounds.Max.X - element.scrollViewportWidth() if element.scroll > maxPosition { element.scroll = maxPosition } - element.redo() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } + element.entity.Invalidate() + element.entity.NotifyScrollBoundsChange() } // ScrollAxes returns the supported axes for scrolling. @@ -376,32 +453,6 @@ func (element *TextBox) ScrollAxes () (horizontal, vertical bool) { return true, false } -func (element *TextBox) runOnChange () { - if element.onChange != nil { - element.onChange() - } -} - -func (element *TextBox) scrollToCursor () { - if !element.core.HasImage() { return } - - padding := element.theme.Padding(tomo.PatternInput) - bounds := padding.Apply(element.Bounds()) - bounds = bounds.Sub(bounds.Min) - bounds.Max.X -= element.valueDrawer.Em().Round() - cursorPosition := fixedutil.RoundPt ( - element.valueDrawer.PositionAt(element.dot.End)) - cursorPosition.X -= element.scroll - maxX := bounds.Max.X - minX := maxX - if cursorPosition.X > maxX { - element.scroll += cursorPosition.X - maxX - } else if cursorPosition.X < minX { - element.scroll -= minX - cursorPosition.X - if element.scroll < 0 { element.scroll = 0 } - } -} - // SetTheme sets the element's theme. func (element *TextBox) SetTheme (new tomo.Theme) { if new == element.theme.Theme { return } @@ -412,7 +463,7 @@ func (element *TextBox) SetTheme (new tomo.Theme) { element.placeholderDrawer.SetFace(face) element.valueDrawer.SetFace(face) element.updateMinimumSize() - element.redo() + element.entity.Invalidate() } // SetConfig sets the element's configuration. @@ -420,13 +471,46 @@ func (element *TextBox) SetConfig (new tomo.Config) { if new == element.config.Config { return } element.config.Config = new element.updateMinimumSize() - element.redo() + element.entity.Invalidate() +} + +func (element *TextBox) runOnChange () { + if element.onChange != nil { + element.onChange() + } +} + +func (element *TextBox) scrollViewportWidth () (width int) { + padding := element.theme.Padding(tomo.PatternInput) + return padding.Apply(element.entity.Bounds()).Dx() +} + +func (element *TextBox) scrollToCursor () { + padding := element.theme.Padding(tomo.PatternInput) + bounds := padding.Apply(element.entity.Bounds()) + bounds = bounds.Sub(bounds.Min) + bounds.Max.X -= element.valueDrawer.Em().Round() + cursorPosition := fixedutil.RoundPt ( + element.valueDrawer.PositionAt(element.dot.End)) + cursorPosition.X -= element.scroll + maxX := bounds.Max.X + minX := maxX + if cursorPosition.X > maxX { + element.scroll += cursorPosition.X - maxX + element.entity.NotifyScrollBoundsChange() + element.entity.Invalidate() + } else if cursorPosition.X < minX { + element.scroll -= minX - cursorPosition.X + if element.scroll < 0 { element.scroll = 0 } + element.entity.NotifyScrollBoundsChange() + element.entity.Invalidate() + } } func (element *TextBox) updateMinimumSize () { textBounds := element.placeholderDrawer.LayoutBounds() padding := element.theme.Padding(tomo.PatternInput) - element.core.SetMinimumSize ( + element.entity.SetMinimumSize ( padding.Horizontal() + textBounds.Dx(), padding.Vertical() + element.placeholderDrawer.LineHeight().Round()) @@ -436,81 +520,19 @@ func (element *TextBox) notifyAsyncTextChange () { element.runOnChange() element.valueDrawer.SetText(element.text) element.scrollToCursor() - if parent, ok := element.core.Parent().(tomo.ScrollableParent); ok { - parent.NotifyScrollBoundsChange(element) - } - element.redo() + element.entity.Invalidate() } -func (element *TextBox) redo () { - if element.core.HasImage () { - element.draw() - element.core.DamageAll() +func (element *TextBox) clipboardPut (text []rune) { + window := element.entity.Window() + if window != nil { + window.Copy(data.Bytes(data.MimePlain, []byte(string(text)))) } } -func (element *TextBox) draw () { - bounds := element.Bounds() - - state := tomo.State { +func (element *TextBox) state () tomo.State { + return tomo.State { Disabled: !element.Enabled(), - Focused: element.Focused(), - } - pattern := element.theme.Pattern(tomo.PatternInput, state) - padding := element.theme.Padding(tomo.PatternInput) - innerCanvas := canvas.Cut(element.core, padding.Apply(bounds)) - pattern.Draw(element.core, bounds) - offset := element.textOffset() - - if element.Focused() && !element.dot.Empty() { - // draw selection bounds - accent := element.theme.Color(tomo.ColorAccent, state) - canon := element.dot.Canon() - foff := fixedutil.Pt(offset) - start := element.valueDrawer.PositionAt(canon.Start).Add(foff) - end := element.valueDrawer.PositionAt(canon.End).Add(foff) - end.Y += element.valueDrawer.LineHeight() - shapes.FillColorRectangle ( - innerCanvas, - accent, - image.Rectangle { - fixedutil.RoundPt(start), - fixedutil.RoundPt(end), - }) - } - - if len(element.text) == 0 { - // draw placeholder - textBounds := element.placeholderDrawer.LayoutBounds() - foreground := element.theme.Color ( - tomo.ColorForeground, - tomo.State { Disabled: true }) - element.placeholderDrawer.Draw ( - innerCanvas, - foreground, - offset.Sub(textBounds.Min)) - } else { - // draw input value - textBounds := element.valueDrawer.LayoutBounds() - foreground := element.theme.Color(tomo.ColorForeground, state) - element.valueDrawer.Draw ( - innerCanvas, - foreground, - offset.Sub(textBounds.Min)) - } - - if element.Focused() && element.dot.Empty() { - // draw cursor - foreground := element.theme.Color(tomo.ColorForeground, state) - cursorPosition := fixedutil.RoundPt ( - element.valueDrawer.PositionAt(element.dot.End)) - shapes.ColorLine ( - innerCanvas, - foreground, 1, - cursorPosition.Add(offset), - image.Pt ( - cursorPosition.X, - cursorPosition.Y + element.valueDrawer. - LineHeight().Round()).Add(offset)) + Focused: element.entity.Focused(), } } diff --git a/entity.go b/entity.go new file mode 100644 index 0000000..bdeb562 --- /dev/null +++ b/entity.go @@ -0,0 +1,129 @@ +package tomo + +import "image" +import "git.tebibyte.media/sashakoshka/tomo/canvas" + +// Entity is a handle given to elements by the backend. Different types of +// entities may be assigned to elements that support different capabilities. +type Entity interface { + // Invalidate marks the element's current visual as invalid. At the end + // of every event, the backend will ask all invalid entities to redraw + // themselves. + Invalidate () + + // Bounds returns the bounds of the element to be used for drawing and + // layout. + Bounds () image.Rectangle + + // Window returns the window that the element is in. + Window () Window + + // SetMinimumSize reports to the system what the element's minimum size + // can be. The minimum size of child elements should be taken into + // account when calculating this. + SetMinimumSize (width, height int) + + // DrawBackground asks the parent element to draw its background pattern + // to a canvas. This should be used for transparent elements like text + // labels. If there is no parent element (that is, the element is + // directly inside of the window), the backend will draw a default + // background pattern. + DrawBackground (canvas.Canvas) +} + +// LayoutEntity is given to elements that support the Layoutable interface. +type LayoutEntity interface { + Entity + + // InvalidateLayout marks the element's layout as invalid. At the end of + // every event, the backend will ask all invalid elements to recalculate + // their layouts. + InvalidateLayout () +} + +// ContainerEntity is given to elements that support the Container interface. +type ContainerEntity interface { + Entity + LayoutEntity + + // Adopt adds an element as a child. + Adopt (child Element) + + // Insert inserts an element in the child list at the specified + // location. + Insert (index int, child Element) + + // Disown removes the child at the specified index. + Disown (index int) + + // IndexOf returns the index of the specified child. + IndexOf (child Element) int + + // Child returns the child at the specified index. + Child (index int) Element + + // CountChildren returns the amount of children the element has. + CountChildren () int + + // PlaceChild sets the size and position of the child at the specified + // index to a bounding rectangle. + PlaceChild (index int, bounds image.Rectangle) + + // SelectChild marks a child as selected or unselected, if it is + // selectable. + SelectChild (index int, selected bool) + + // ChildMinimumSize returns the minimum size of the child at the + // specified index. + ChildMinimumSize (index int) (width, height int) +} + +// FocusableEntity is given to elements that support the Focusable interface. +type FocusableEntity interface { + Entity + + // Focused returns whether the element currently has input focus. + Focused () bool + + // Focus sets this element as focused. If this succeeds, the element will + // recieve a HandleFocus call. + Focus () + + // FocusNext causes the focus to move to the next element. If this + // succeeds, the element will recieve a HandleUnfocus call. + FocusNext () + + // FocusPrevious causes the focus to move to the next element. If this + // succeeds, the element will recieve a HandleUnfocus call. + FocusPrevious () +} + +// SelectableEntity is given to elements that support the Selectable interface. +type SelectableEntity interface { + Entity + + // Selected returns whether this element is currently selected. + Selected () bool +} + +// FlexibleEntity is given to elements that support the Flexible interface. +type FlexibleEntity interface { + Entity + + // NotifyFlexibleHeightChange notifies the system that the parameters + // affecting the element's flexible height have changed. This method is + // expected to be called by flexible elements when their content changes. + NotifyFlexibleHeightChange () +} + +// ScrollableEntity is given to elements that support the Scrollable interface. +type ScrollableEntity interface { + Entity + + // NotifyScrollBoundsChange notifies the system that the element's + // scroll content bounds or viewport bounds have changed. This is + // expected to be called by scrollable elements when they change their + // supported scroll axes, their scroll position (either autonomously or + // as a result of a call to ScrollTo()), or their content size. + NotifyScrollBoundsChange () +} diff --git a/examples/align/main.go b/examples/align/main.go index 1d0c77d..1857b42 100644 --- a/examples/align/main.go +++ b/examples/align/main.go @@ -4,35 +4,27 @@ import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/textdraw" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) } func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) + window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 256)) window.SetTitle("Text alignment") - container := containers.NewDocumentContainer() - scrollContainer := containers.NewScrollContainer(false, true) - scrollContainer.Adopt(container) - window.Adopt(scrollContainer) - - left := elements.NewLabel(text, true) - center := elements.NewLabel(text, true) - right := elements.NewLabel(text, true) - justify := elements.NewLabel(text, true) + left := elements.NewLabelWrapped(text) + center := elements.NewLabelWrapped(text) + right := elements.NewLabelWrapped(text) + justify := elements.NewLabelWrapped(text) left.SetAlign(textdraw.AlignLeft) center.SetAlign(textdraw.AlignCenter) right.SetAlign(textdraw.AlignRight) justify.SetAlign(textdraw.AlignJustify) - container.Adopt(left, true) - container.Adopt(center, true) - container.Adopt(right, true) - container.Adopt(justify, true) + window.Adopt (elements.NewScroll (elements.ScrollVertical, + elements.NewDocument(left, center, right, justify))) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/checkbox/main.go b/examples/checkbox/main.go index a0dfbf4..549e54d 100644 --- a/examples/checkbox/main.go +++ b/examples/checkbox/main.go @@ -2,9 +2,7 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -15,23 +13,15 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Checkboxes") - container := containers.NewContainer(layouts.Vertical { true, true }) - window.Adopt(container) - - introText := elements.NewLabel ( + introText := elements.NewLabelWrapped ( "We advise you to not read thPlease listen to me. I am " + "trapped inside the example code. This is the only way for " + - "me to communicate.", true) + "me to communicate.") introText.EmCollapse(0, 5) - container.Adopt(introText, true) - container.Adopt(elements.NewSpacer(true), false) - container.Adopt(elements.NewCheckbox("Oh god", false), false) - container.Adopt(elements.NewCheckbox("Can you hear them", true), false) - container.Adopt(elements.NewCheckbox("They are in the walls", false), false) - container.Adopt(elements.NewCheckbox("They are coming for us", false), false) + disabledCheckbox := elements.NewCheckbox("We are but their helpless prey", false) disabledCheckbox.SetEnabled(false) - container.Adopt(disabledCheckbox, false) + vsync := elements.NewCheckbox("Enable vsync", false) vsync.OnToggle (func () { if vsync.Value() { @@ -42,12 +32,23 @@ func run () { "That doesn't do anything.") } }) - container.Adopt(vsync, false) + button := elements.NewButton("What") button.OnClick(tomo.Stop) - container.Adopt(button, false) - button.Focus() + + box := elements.NewVBox(elements.SpaceBoth) + box.AdoptExpand(introText) + box.Adopt ( + elements.NewLine(), + elements.NewCheckbox("Oh god", false), + elements.NewCheckbox("Can you hear them", true), + elements.NewCheckbox("They are in the walls", false), + elements.NewCheckbox("They are coming for us", false), + disabledCheckbox, + vsync, button) + window.Adopt(box) + button.Focus() window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/clipboard/main.go b/examples/clipboard/main.go index 40eb0df..4102686 100644 --- a/examples/clipboard/main.go +++ b/examples/clipboard/main.go @@ -8,10 +8,8 @@ import _ "image/jpeg" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/data" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -27,9 +25,9 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 256, 0)) window.SetTitle("Clipboard") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) textInput := elements.NewTextBox("", "") - controlRow := containers.NewContainer(layouts.Horizontal { true, false }) + controlRow := elements.NewHBox(elements.SpaceMargin) copyButton := elements.NewButton("Copy") copyButton.SetIcon(tomo.IconCopy) pasteButton := elements.NewButton("Paste") @@ -109,11 +107,11 @@ func run () { window.Paste(imageClipboardCallback, validImageTypes...) }) - container.Adopt(textInput, true) - controlRow.Adopt(copyButton, true) - controlRow.Adopt(pasteButton, true) - controlRow.Adopt(pasteImageButton, true) - container.Adopt(controlRow, false) + container.AdoptExpand(textInput) + controlRow.AdoptExpand(copyButton) + controlRow.AdoptExpand(pasteButton) + controlRow.AdoptExpand(pasteImageButton) + container.Adopt(controlRow) window.Adopt(container) window.OnClose(tomo.Stop) @@ -123,13 +121,15 @@ func run () { func imageWindow (parent tomo.Window, image image.Image) { window, _ := parent.NewModal(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Clipboard Image") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) closeButton := elements.NewButton("Ok") closeButton.SetIcon(tomo.IconYes) closeButton.OnClick(window.Close) - container.Adopt(elements.NewImage(image), true) - container.Adopt(closeButton, false) + container.AdoptExpand(elements.NewImage(image)) + container.Adopt(closeButton) window.Adopt(container) + + closeButton.Focus() window.Show() } diff --git a/examples/dialogLayout/main.go b/examples/dialogLayout/main.go deleted file mode 100644 index 519ae4a..0000000 --- a/examples/dialogLayout/main.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" -import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" - -func main () { - tomo.Run(run) -} - -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) - window.SetTitle("dialog") - - container := containers.NewContainer(layouts.Dialog { true, true }) - window.Adopt(container) - - container.Adopt(elements.NewLabel("you will explode", false), true) - cancel := elements.NewButton("Cancel") - cancel.SetEnabled(false) - container.Adopt(cancel, false) - okButton := elements.NewButton("OK") - container.Adopt(okButton, false) - okButton.Focus() - - window.OnClose(tomo.Stop) - window.Show() -} diff --git a/examples/documentContainer/main.go b/examples/documentContainer/main.go index 37a2a3f..146137b 100644 --- a/examples/documentContainer/main.go +++ b/examples/documentContainer/main.go @@ -6,7 +6,6 @@ import _ "image/png" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -22,47 +21,41 @@ func run () { file.Close() if err != nil { panic(err.Error()); return } - scrollContainer := containers.NewScrollContainer(false, true) - document := containers.NewDocumentContainer() - - document.Adopt (elements.NewLabel ( - "A document container is a vertically stacked container " + - "capable of properly laying out flexible elements such as " + - "text-wrapped labels. You can also include normal elements " + - "like:", true), true) - document.Adopt (elements.NewButton ( - "Buttons,"), true) - document.Adopt (elements.NewCheckbox ( - "Checkboxes,", true), true) - document.Adopt(elements.NewTextBox("", "And text boxes."), true) - document.Adopt (elements.NewSpacer(true), true) - document.Adopt (elements.NewLabel ( - "Document containers are meant to be placed inside of a " + - "ScrollContainer, like this one.", true), true) - document.Adopt (elements.NewLabel ( - "You could use document containers to do things like display various " + - "forms of hypertext (like HTML, gemtext, markdown, etc.), " + - "lay out a settings menu with descriptive label text between " + - "control groups like in iOS, or list comment or chat histories.", - true), true) - document.Adopt(elements.NewImage(logo), true) - document.Adopt (elements.NewLabel ( - "You can also choose whether each element is on its own line " + - "(sort of like an HTML/CSS block element) or on a line with " + - "other adjacent elements (like an HTML/CSS inline element).", - true), true) - document.Adopt(elements.NewButton("Just"), false) - document.Adopt(elements.NewButton("like"), false) - document.Adopt(elements.NewButton("this."), false) - document.Adopt (elements.NewLabel ( - "Oh, you're a switch? Then name all of these switches:", - true), true) + document := elements.NewDocument() + document.Adopt ( + elements.NewLabelWrapped ( + "A document container is a vertically stacked container " + + "capable of properly laying out flexible elements such as " + + "text-wrapped labels. You can also include normal elements " + + "like:"), + elements.NewButton("Buttons,"), + elements.NewCheckbox("Checkboxes,", true), + elements.NewTextBox("", "And text boxes."), + elements.NewLine(), + elements.NewLabelWrapped ( + "Document containers are meant to be placed inside of a " + + "ScrollContainer, like this one."), + elements.NewLabelWrapped ( + "You could use document containers to do things like display various " + + "forms of hypertext (like HTML, gemtext, markdown, etc.), " + + "lay out a settings menu with descriptive label text between " + + "control groups like in iOS, or list comment or chat histories."), + elements.NewImage(logo), + elements.NewLabelWrapped ( + "You can also choose whether each element is on its own line " + + "(sort of like an HTML/CSS block element) or on a line with " + + "other adjacent elements (like an HTML/CSS inline element).")) + document.AdoptInline ( + elements.NewButton("Just"), + elements.NewButton("like"), + elements.NewButton("this.")) + document.Adopt (elements.NewLabelWrapped ( + "Oh, you're a switch? Then name all of these switches:")) for i := 0; i < 30; i ++ { - document.Adopt(elements.NewSwitch("", false), false) + document.AdoptInline(elements.NewSwitch("", false)) } - scrollContainer.Adopt(document) - window.Adopt(scrollContainer) + window.Adopt(elements.NewScroll(elements.ScrollVertical, document)) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/fileBrowser/main.go b/examples/fileBrowser/main.go index 156aab1..842df94 100644 --- a/examples/fileBrowser/main.go +++ b/examples/fileBrowser/main.go @@ -3,10 +3,7 @@ package main import "os" import "path/filepath" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/file" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -16,11 +13,11 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 384, 384)) window.SetTitle("File browser") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) homeDir, _ := os.UserHomeDir() - controlBar := containers.NewContainer(layouts.Horizontal { }) + controlBar := elements.NewHBox(elements.SpaceNone) backButton := elements.NewButton("Back") backButton.SetIcon(tomo.IconBackward) backButton.ShowText(false) @@ -35,12 +32,11 @@ func run () { upwardButton.ShowText(false) locationInput := elements.NewTextBox("Location", "") - statusBar := containers.NewContainer(layouts.Horizontal { true, false }) - directory, _ := fileElements.NewFile(homeDir, nil) - baseName := elements.NewLabel(filepath.Base(homeDir), false) + statusBar := elements.NewHBox(elements.SpaceMargin) + directory, _ := elements.NewFile(homeDir, nil) + baseName := elements.NewLabel(filepath.Base(homeDir)) - scrollContainer := containers.NewScrollContainer(false, true) - directoryView, _ := fileElements.NewDirectory(homeDir, nil) + directoryView, _ := elements.NewDirectory(homeDir, nil) updateStatus := func () { filePath, _ := directoryView.Location() directory.SetLocation(filePath, nil) @@ -72,19 +68,15 @@ func run () { filePath, _ := directoryView.Location() choose(filepath.Dir(filePath)) }) + + controlBar.Adopt(backButton, forwardButton, refreshButton, upwardButton) + controlBar.AdoptExpand(locationInput) + statusBar.Adopt(directory, baseName) - controlBar.Adopt(backButton, false) - controlBar.Adopt(forwardButton, false) - controlBar.Adopt(refreshButton, false) - controlBar.Adopt(upwardButton, false) - controlBar.Adopt(locationInput, true) - scrollContainer.Adopt(directoryView) - statusBar.Adopt(directory, false) - statusBar.Adopt(baseName, false) - - container.Adopt(controlBar, false) - container.Adopt(scrollContainer, true) - container.Adopt(statusBar, false) + container.Adopt(controlBar) + container.AdoptExpand ( + elements.NewScroll(elements.ScrollVertical, directoryView)) + container.Adopt(statusBar) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/flow/main.go b/examples/flow/main.go index 565f614..dac69cc 100644 --- a/examples/flow/main.go +++ b/examples/flow/main.go @@ -2,10 +2,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/flow" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -14,15 +12,15 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 192, 192)) window.SetTitle("adventure") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) var world flow.Flow world.Transition = container.DisownAll world.Stages = map [string] func () { "start": func () { - label := elements.NewLabel ( - "you are standing next to a river.", true) + label := elements.NewLabelWrapped ( + "you are standing next to a river.") button0 := elements.NewButton("go in the river") button0.OnClick(world.SwitchFunc("wet")) @@ -31,81 +29,66 @@ func run () { button2 := elements.NewButton("turn around") button2.OnClick(world.SwitchFunc("bear")) - container.Warp ( func () { - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) - container.Adopt(button2, false) - button0.Focus() - }) + container.AdoptExpand(label) + container.Adopt(button0, button1, button2) + button0.Focus() }, "wet": func () { - label := elements.NewLabel ( + label := elements.NewLabelWrapped ( "you get completely soaked.\n" + - "you die of hypothermia.", true) + "you die of hypothermia.") button0 := elements.NewButton("try again") button0.OnClick(world.SwitchFunc("start")) button1 := elements.NewButton("exit") button1.OnClick(tomo.Stop) - container.Warp (func () { - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) - button0.Focus() - }) + container.AdoptExpand(label) + container.Adopt(button0, button1) + button0.Focus() }, "house": func () { - label := elements.NewLabel ( + label := elements.NewLabelWrapped ( "you are standing in front of a delapidated " + - "house.", true) + "house.") button1 := elements.NewButton("go inside") button1.OnClick(world.SwitchFunc("inside")) button0 := elements.NewButton("turn back") button0.OnClick(world.SwitchFunc("start")) - container.Warp (func () { - container.Adopt(label, true) - container.Adopt(button1, false) - container.Adopt(button0, false) - button1.Focus() - }) + container.AdoptExpand(label) + container.Adopt(button0, button1) + button1.Focus() }, "inside": func () { - label := elements.NewLabel ( + label := elements.NewLabelWrapped ( "you are standing inside of the house.\n" + "it is dark, but rays of light stream " + "through the window.\n" + "there is nothing particularly interesting " + - "here.", true) + "here.") button0 := elements.NewButton("go back outside") button0.OnClick(world.SwitchFunc("house")) - container.Warp (func () { - container.Adopt(label, true) - container.Adopt(button0, false) - button0.Focus() - }) + container.AdoptExpand(label) + container.Adopt(button0) + button0.Focus() }, "bear": func () { - label := elements.NewLabel ( + label := elements.NewLabelWrapped ( "you come face to face with a bear.\n" + - "it eats you (it was hungry).", true) + "it eats you (it was hungry).") button0 := elements.NewButton("try again") button0.OnClick(world.SwitchFunc("start")) button1 := elements.NewButton("exit") button1.OnClick(tomo.Stop) - container.Warp (func () { - container.Adopt(label, true) - container.Adopt(button0, false) - container.Adopt(button1, false) - button0.Focus() - }) + container.AdoptExpand(label) + container.Adopt(button0, button1) + button0.Focus() }, } world.Switch("start") diff --git a/examples/goroutines/main.go b/examples/goroutines/main.go index caa41bb..bfbb508 100644 --- a/examples/goroutines/main.go +++ b/examples/goroutines/main.go @@ -3,11 +3,9 @@ package main import "os" import "time" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/fun" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -17,13 +15,14 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 200, 216)) window.SetTitle("Clock") - container := containers.NewContainer(layouts.Vertical { true, true }) + window.SetApplicationName("TomoClock") + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) clock := fun.NewAnalogClock(time.Now()) - container.Adopt(clock, true) - label := elements.NewLabel(formatTime(), false) - container.Adopt(label, false) + label := elements.NewLabel(formatTime()) + container.AdoptExpand(clock) + container.Adopt(label) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/hbox/main.go b/examples/hbox/main.go new file mode 100644 index 0000000..b469ae7 --- /dev/null +++ b/examples/hbox/main.go @@ -0,0 +1,24 @@ +package main + +import "git.tebibyte.media/sashakoshka/tomo" +import "git.tebibyte.media/sashakoshka/tomo/elements" +import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" + +func main () { + tomo.Run(run) +} + +func run () { + window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) + window.SetTitle("horizontal stack") + + container := elements.NewHBox(elements.SpaceBoth) + window.Adopt(container) + + container.AdoptExpand(elements.NewLabelWrapped("this is sample text")) + container.AdoptExpand(elements.NewLabelWrapped("this is sample text")) + container.AdoptExpand(elements.NewLabelWrapped("this is sample text")) + + window.OnClose(tomo.Stop) + window.Show() +} diff --git a/examples/horizontalLayout/main.go b/examples/horizontalLayout/main.go deleted file mode 100644 index c55a424..0000000 --- a/examples/horizontalLayout/main.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" -import "git.tebibyte.media/sashakoshka/tomo/elements" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" - -func main () { - tomo.Run(run) -} - -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) - window.SetTitle("horizontal stack") - - container := containers.NewContainer(layouts.Horizontal { true, true }) - window.Adopt(container) - - container.Adopt(elements.NewLabel("this is sample text", true), true) - container.Adopt(elements.NewLabel("this is sample text", true), true) - container.Adopt(elements.NewLabel("this is sample text", true), true) - - window.OnClose(tomo.Stop) - window.Show() -} diff --git a/examples/icons/main.go b/examples/icons/main.go index 52c45cf..01ed2f5 100644 --- a/examples/icons/main.go +++ b/examples/icons/main.go @@ -1,10 +1,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -14,30 +12,31 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 0)) window.SetTitle("Icons") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt(elements.NewLabel("Just some of the wonderful icons we have:", false), false) - container.Adopt(elements.NewSpacer(true), false) - container.Adopt(icons(tomo.IconHome, tomo.IconHistory), true) - container.Adopt(icons(tomo.IconFile, tomo.IconNetwork), true) - container.Adopt(icons(tomo.IconOpen, tomo.IconRemoveFavorite), true) - container.Adopt(icons(tomo.IconCursor, tomo.IconDistort), true) + container.Adopt ( + elements.NewLabel("Just some of the wonderful icons we have:"), + elements.NewLine()) + container.AdoptExpand ( + icons(tomo.IconHome, tomo.IconHistory), + icons(tomo.IconFile, tomo.IconNetwork), + icons(tomo.IconOpen, tomo.IconRemoveFavorite), + icons(tomo.IconCursor, tomo.IconDistort)) - closeButton := elements.NewButton("Ok") + closeButton := elements.NewButton("Yes verynice") closeButton.SetIcon(tomo.IconYes) - closeButton.ShowText(false) closeButton.OnClick(tomo.Stop) - container.Adopt(closeButton, false) + container.Adopt(closeButton) window.OnClose(tomo.Stop) window.Show() } -func icons (min, max tomo.Icon) (container *containers.Container) { - container = containers.NewContainer(layouts.Horizontal { true, false }) +func icons (min, max tomo.Icon) (container *elements.Box) { + container = elements.NewHBox(elements.SpaceMargin) for index := min; index <= max; index ++ { - container.Adopt(elements.NewIcon(index, tomo.IconSizeSmall), true) + container.AdoptExpand(elements.NewIcon(index, tomo.IconSizeSmall)) } return } diff --git a/examples/image/image.go b/examples/image/image.go index 7d78756..0dc2100 100644 --- a/examples/image/image.go +++ b/examples/image/image.go @@ -7,17 +7,15 @@ import _ "image/png" import "github.com/jezek/xgbutil/gopher" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) } func run () { - window, _ := tomo.NewWindow(2, 2) + window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Tomo Logo") file, err := os.Open("assets/banner.png") @@ -26,19 +24,20 @@ func run () { file.Close() if err != nil { fatalError(window, err); return } - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) logoImage := elements.NewImage(logo) button := elements.NewButton("Show me a gopher instead") - button.OnClick (func () { container.Warp (func () { - container.DisownAll() - gopher, _, err := - image.Decode(bytes.NewReader(gopher.GopherPng())) - if err != nil { fatalError(window, err); return } - container.Adopt(elements.NewImage(gopher),true) - }) }) + button.OnClick (func () { + window.SetTitle("Not the Tomo Logo") + container.DisownAll() + gopher, _, err := + image.Decode(bytes.NewReader(gopher.GopherPng())) + if err != nil { fatalError(window, err); return } + container.AdoptExpand(elements.NewImage(gopher)) + }) - container.Adopt(logoImage, true) - container.Adopt(button, false) + container.AdoptExpand(logoImage) + container.Adopt(button) window.Adopt(container) button.Focus() diff --git a/examples/input/main.go b/examples/input/main.go index 1016493..e8035c0 100644 --- a/examples/input/main.go +++ b/examples/input/main.go @@ -2,10 +2,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -14,7 +12,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Enter Details") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) // create inputs @@ -47,13 +45,8 @@ func run () { fingerLength.OnChange(check) // add elements to container - container.Adopt(elements.NewLabel("Choose your words carefully.", false), true) - container.Adopt(firstName, false) - container.Adopt(lastName, false) - container.Adopt(fingerLength, false) - container.Adopt(elements.NewSpacer(true), false) - container.Adopt(button, false) - + container.AdoptExpand(elements.NewLabel("Choose your words carefully.")) + container.Adopt(firstName, lastName, fingerLength, elements.NewLine(), button) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/label/main.go b/examples/label/main.go index 0429587..96dc6a8 100644 --- a/examples/label/main.go +++ b/examples/label/main.go @@ -11,7 +11,7 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 480, 360)) window.SetTitle("example label") - window.Adopt(elements.NewLabel(text, true)) + window.Adopt(elements.NewLabelWrapped(text)) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/list/main.go b/examples/list/main.go index 0aeeec8..1e4fbc4 100644 --- a/examples/list/main.go +++ b/examples/list/main.go @@ -2,10 +2,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/testing" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -16,47 +14,54 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 300, 0)) window.SetTitle("List Sidebar") - container := containers.NewContainer(layouts.Horizontal { true, true }) + container := elements.NewHBox(elements.SpaceBoth) window.Adopt(container) var currentPage tomo.Element turnPage := func (newPage tomo.Element) { - container.Warp (func () { - if currentPage != nil { - container.Disown(currentPage) - } - container.Adopt(newPage, true) - currentPage = newPage - }) + if currentPage != nil { + container.Disown(currentPage) + } + container.AdoptExpand(newPage) + currentPage = newPage } - intro := elements.NewLabel ( + intro := elements.NewLabelWrapped ( "The List element can be easily used as a sidebar. " + - "Click on entries to flip pages!", true) + "Click on entries to flip pages!") button := elements.NewButton("I do nothing!") button.OnClick (func () { popups.NewDialog(popups.DialogKindInfo, window, "", "Sike!") }) mouse := testing.NewMouse() input := elements.NewTextBox("Write some text", "") - form := containers.NewContainer(layouts.Vertical { true, false}) - form.Adopt(elements.NewLabel("I have:", false), false) - form.Adopt(elements.NewSpacer(true), false) - form.Adopt(elements.NewCheckbox("Skin", true), false) - form.Adopt(elements.NewCheckbox("Blood", false), false) - form.Adopt(elements.NewCheckbox("Bone", false), false) + form := elements.NewVBox ( + elements.SpaceMargin, + elements.NewLabel("I have:"), + elements.NewLine(), + elements.NewCheckbox("Skin", true), + elements.NewCheckbox("Blood", false), + elements.NewCheckbox("Bone", false)) art := testing.NewArtist() + makePage := func (name string, callback func ()) tomo.Selectable { + cell := elements.NewCell(elements.NewLabel(name)) + cell.OnSelectionChange (func () { + if cell.Selected() { callback() } + }) + return cell + } + list := elements.NewList ( - elements.NewListEntry("button", func () { turnPage(button) }), - elements.NewListEntry("mouse", func () { turnPage(mouse) }), - elements.NewListEntry("input", func () { turnPage(input) }), - elements.NewListEntry("form", func () { turnPage(form) }), - elements.NewListEntry("art", func () { turnPage(art) })) - list.OnNoEntrySelected(func () { turnPage (intro) }) + 1, + makePage("button", func () { turnPage(button) }), + makePage("mouse", func () { turnPage(mouse) }), + makePage("input", func () { turnPage(input) }), + makePage("form", func () { turnPage(form) }), + makePage("art", func () { turnPage(art) })) list.Collapse(96, 0) - container.Adopt(list, false) + container.Adopt(list) turnPage(intro) window.OnClose(tomo.Stop) diff --git a/examples/panels/main.go b/examples/panels/main.go index 4997c8f..b28b204 100644 --- a/examples/panels/main.go +++ b/examples/panels/main.go @@ -3,10 +3,8 @@ package main import "fmt" import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -16,8 +14,9 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(200, 200, 256, 256)) window.SetTitle("Main") - container := containers.NewContainer(layouts.Vertical { true, true }) - container.Adopt(elements.NewLabel("Main window", false), true) + container := elements.NewVBox ( + elements.SpaceBoth, + elements.NewLabel("Main window")) window.Adopt(container) window.OnClose(tomo.Stop) @@ -33,8 +32,9 @@ func createPanel (parent tomo.MainWindow, id int, bounds image.Rectangle) { window, _ := parent.NewPanel(bounds) title := fmt.Sprint("Panel #", id) window.SetTitle(title) - container := containers.NewContainer(layouts.Vertical { true, true }) - container.Adopt(elements.NewLabel(title, false), true) + container := elements.NewVBox ( + elements.SpaceBoth, + elements.NewLabel(title)) window.Adopt(container) window.Show() } diff --git a/examples/popups/main.go b/examples/popups/main.go index 414681d..4b2c83a 100644 --- a/examples/popups/main.go +++ b/examples/popups/main.go @@ -2,10 +2,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -16,10 +14,10 @@ func run () { if err != nil { panic(err.Error()) } window.SetTitle("Dialog Boxes") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt(elements.NewLabel("Try out different dialogs:", false), true) + container.AdoptExpand(elements.NewLabel("Try out different dialogs:")) infoButton := elements.NewButton("popups.DialogKindInfo") infoButton.OnClick (func () { @@ -29,7 +27,7 @@ func run () { "Information", "You are wacky") }) - container.Adopt(infoButton, false) + container.Adopt(infoButton) infoButton.Focus() questionButton := elements.NewButton("popups.DialogKindQuestion") @@ -43,7 +41,7 @@ func run () { popups.Button { "No", func () { } }, popups.Button { "Not sure", func () { } }) }) - container.Adopt(questionButton, false) + container.Adopt(questionButton) warningButton := elements.NewButton("popups.DialogKindWarning") warningButton.OnClick (func () { @@ -53,7 +51,7 @@ func run () { "Warning", "They are fast approaching.") }) - container.Adopt(warningButton, false) + container.Adopt(warningButton) errorButton := elements.NewButton("popups.DialogKindError") errorButton.OnClick (func () { @@ -63,22 +61,23 @@ func run () { "Error", "There is nowhere left to go.") }) - container.Adopt(errorButton, false) + container.Adopt(errorButton) menuButton := elements.NewButton("menu") menuButton.OnClick (func () { + // TODO: make a better way to get the bounds of something menu, err := window.NewMenu ( tomo.Bounds(0, 0, 64, 64). - Add(menuButton.Bounds().Min)) + Add(menuButton.Entity().Bounds().Min)) if err != nil { println(err.Error()) } - menu.Adopt(elements.NewLabel("I'm a shy window...", true)) + menu.Adopt(elements.NewLabelWrapped("I'm a shy window...")) menu.Show() }) - container.Adopt(menuButton, false) + container.Adopt(menuButton) cancelButton := elements.NewButton("No thank you.") cancelButton.OnClick(tomo.Stop) - container.Adopt(cancelButton, false) + container.Adopt(cancelButton) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/progress/main.go b/examples/progress/main.go index 8e0a630..0fad66b 100644 --- a/examples/progress/main.go +++ b/examples/progress/main.go @@ -3,9 +3,7 @@ package main import "time" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -15,16 +13,15 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Approaching") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt (elements.NewLabel ( - "Rapidly approaching your location...", false), false) + container.AdoptExpand(elements.NewLabel("Rapidly approaching your location...")) bar := elements.NewProgressBar(0) - container.Adopt(bar, false) + container.Adopt(bar) button := elements.NewButton("Stop") button.SetEnabled(false) - container.Adopt(button, false) + container.Adopt(button) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/raycaster/game.go b/examples/raycaster/game.go index 2c66cf3..35d804a 100644 --- a/examples/raycaster/game.go +++ b/examples/raycaster/game.go @@ -1,9 +1,7 @@ package main import "time" -import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/canvas" type Game struct { *Raycaster @@ -31,21 +29,17 @@ func NewGame (world World, textures Textures) (game *Game) { return } -func (game *Game) DrawTo ( - canvas canvas.Canvas, - bounds image.Rectangle, - onDamage func (image.Rectangle), -) { - if canvas == nil { - select { - case game.stopChan <- true: - default: - } - } else if !game.running { - game.running = true - go game.run() - } - game.Raycaster.DrawTo(canvas, bounds, onDamage) +func (game *Game) Start () { + if game.running == true { return } + game.running = true + go game.run() +} + +func (game *Game) Stop () { + select { + case game.stopChan <- true: + default: + } } func (game *Game) Stamina () float64 { @@ -109,8 +103,8 @@ func (game *Game) tick () { if game.stamina < 0 { game.stamina = 0 } - - tomo.Do(game.Draw) + + tomo.Do(game.Invalidate) if statUpdate && game.onStatUpdate != nil { tomo.Do(game.onStatUpdate) } diff --git a/examples/raycaster/main.go b/examples/raycaster/main.go index 69256d7..f848c99 100644 --- a/examples/raycaster/main.go +++ b/examples/raycaster/main.go @@ -5,10 +5,8 @@ import _ "embed" import _ "image/png" import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/popups" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" //go:embed wall.png var wallTextureBytes []uint8 @@ -17,11 +15,13 @@ func main () { tomo.Run(run) } +// FIXME this entire example seems to be broken + func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 640, 480)) window.SetTitle("Raycaster") - container := containers.NewContainer(layouts.Vertical { false, false }) + container := elements.NewVBox(elements.SpaceNone) window.Adopt(container) wallTexture, _ := TextureFrom(bytes.NewReader(wallTextureBytes)) @@ -48,21 +48,22 @@ func run () { wallTexture, }) - topBar := containers.NewContainer(layouts.Horizontal { true, true }) + topBar := elements.NewHBox(elements.SpaceBoth) staminaBar := elements.NewProgressBar(game.Stamina()) healthBar := elements.NewProgressBar(game.Health()) - topBar.Adopt(elements.NewLabel("Stamina:", false), false) - topBar.Adopt(staminaBar, true) - topBar.Adopt(elements.NewLabel("Health:", false), false) - topBar.Adopt(healthBar, true) - container.Adopt(topBar, false) - container.Adopt(game, true) + topBar.Adopt(elements.NewLabel("Stamina:")) + topBar.AdoptExpand(staminaBar) + topBar.Adopt(elements.NewLabel("Health:")) + topBar.AdoptExpand(healthBar) + container.Adopt(topBar) + container.AdoptExpand(game.Raycaster) game.Focus() game.OnStatUpdate (func () { staminaBar.SetProgress(game.Stamina()) }) + game.Start() window.OnClose(tomo.Stop) window.Show() diff --git a/examples/raycaster/raycaster.go b/examples/raycaster/raycaster.go index bef8070..4360ef4 100644 --- a/examples/raycaster/raycaster.go +++ b/examples/raycaster/raycaster.go @@ -4,10 +4,11 @@ package main import "math" import "image" import "image/color" +import "git.tebibyte.media/sashakoshka/tomo" import "git.tebibyte.media/sashakoshka/tomo/input" +import "git.tebibyte.media/sashakoshka/tomo/canvas" import "git.tebibyte.media/sashakoshka/tomo/artist" import "git.tebibyte.media/sashakoshka/tomo/artist/shapes" -import "git.tebibyte.media/sashakoshka/tomo/elements/core" import "git.tebibyte.media/sashakoshka/tomo/default/config" type ControlState struct { @@ -21,10 +22,8 @@ type ControlState struct { } type Raycaster struct { - *core.Core - *core.FocusableCore - core core.CoreControl - focusableControl core.FocusableCoreControl + entity tomo.FocusableEntity + config config.Wrapped Camera @@ -49,31 +48,107 @@ func NewRaycaster (world World, textures Textures) (element *Raycaster) { textures: textures, renderDistance: 8, } - element.Core, element.core = core.NewCore(element, element.drawAll) - element.FocusableCore, - element.focusableControl = core.NewFocusableCore(element.core, element.Draw) - element.core.SetMinimumSize(64, 64) + element.entity = tomo.NewEntity(element).(tomo.FocusableEntity) + element.entity.SetMinimumSize(64, 64) return } +func (element *Raycaster) Entity () tomo.Entity { + return element.entity +} + +func (element *Raycaster) Draw (destination canvas.Canvas) { + bounds := element.entity.Bounds() + // artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds) + width := bounds.Dx() + height := bounds.Dy() + halfway := bounds.Max.Y - height / 2 + + ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 } + + for x := 0; x < width; x ++ { + ray.X = element.Camera.X + ray.Y = element.Camera.Y + + distance, hitPoint, wall, horizontal := ray.Cast ( + element.world, element.renderDistance) + distance *= math.Cos(ray.Angle - element.Camera.Angle) + textureX := math.Mod(hitPoint.X + hitPoint.Y, 1) + if textureX < 0 { textureX += 1 } + + wallHeight := height + if distance > 0 { + wallHeight = int((float64(height) / 2.0) / float64(distance)) + } + + shade := 1.0 + if horizontal { + shade *= 0.8 + } + shade *= 1 - distance / float64(element.renderDistance) + if shade < 0 { shade = 0 } + + ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF } + floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF } + + // draw + data, stride := destination.Buffer() + wallStart := halfway - wallHeight + wallEnd := halfway + wallHeight + + for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { + switch { + case y < wallStart: + data[y * stride + x + bounds.Min.X] = ceilingColor + + case y < wallEnd: + textureY := + float64(y - halfway) / + float64(wallEnd - wallStart) + 0.5 + // fmt.Println(textureY) + + wallColor := element.textures.At (wall, Vector { + textureX, + textureY, + }) + wallColor = shadeColor(wallColor, shade) + data[y * stride + x + bounds.Min.X] = wallColor + + default: + data[y * stride + x + bounds.Min.X] = floorColor + } + } + + // increment angle + ray.Angle += element.Camera.Fov / float64(width) + } + + // element.drawMinimap() +} + +func (element *Raycaster) Invalidate () { + element.entity.Invalidate() +} + func (element *Raycaster) OnControlStateChange (callback func (ControlState)) { element.onControlStateChange = callback } -func (element *Raycaster) Draw () { - if element.core.HasImage() { - element.drawAll() - element.core.DamageAll() - } +func (element *Raycaster) Focus () { + element.entity.Focus() } +func (element *Raycaster) SetEnabled (bool) { } + +func (element *Raycaster) Enabled () bool { return true } + +func (element *Raycaster) HandleFocusChange () { } + func (element *Raycaster) HandleMouseDown (x, y int, button input.Button) { - if !element.Focused() { element.Focus() } + element.entity.Focus() } func (element *Raycaster) HandleMouseUp (x, y int, button input.Button) { } -func (element *Raycaster) HandleMouseMove (x, y int) { } -func (element *Raycaster) HandleMouseScroll (x, y int, deltaX, deltaY float64) { } func (element *Raycaster) HandleKeyDown (key input.Key, modifiers input.Modifiers) { switch key { @@ -109,75 +184,6 @@ func (element *Raycaster) HandleKeyUp(key input.Key, modifiers input.Modifiers) } } -func (element *Raycaster) drawAll () { - bounds := element.Bounds() - // artist.FillRectangle(element.core, artist.Uhex(0x000000FF), bounds) - width := bounds.Dx() - height := bounds.Dy() - halfway := bounds.Max.Y - height / 2 - - ray := Ray { Angle: element.Camera.Angle - element.Camera.Fov / 2 } - - for x := 0; x < width; x ++ { - ray.X = element.Camera.X - ray.Y = element.Camera.Y - - distance, hitPoint, wall, horizontal := ray.Cast ( - element.world, element.renderDistance) - distance *= math.Cos(ray.Angle - element.Camera.Angle) - textureX := math.Mod(hitPoint.X + hitPoint.Y, 1) - if textureX < 0 { textureX += 1 } - - wallHeight := height - if distance > 0 { - wallHeight = int((float64(height) / 2.0) / float64(distance)) - } - - shade := 1.0 - if horizontal { - shade *= 0.8 - } - shade *= 1 - distance / float64(element.renderDistance) - if shade < 0 { shade = 0 } - - ceilingColor := color.RGBA { 0x00, 0x00, 0x00, 0xFF } - floorColor := color.RGBA { 0x39, 0x49, 0x25, 0xFF } - - // draw - data, stride := element.core.Buffer() - wallStart := halfway - wallHeight - wallEnd := halfway + wallHeight - - for y := bounds.Min.Y; y < bounds.Max.Y; y ++ { - switch { - case y < wallStart: - data[y * stride + x + bounds.Min.X] = ceilingColor - - case y < wallEnd: - textureY := - float64(y - halfway) / - float64(wallEnd - wallStart) + 0.5 - // fmt.Println(textureY) - - wallColor := element.textures.At (wall, Vector { - textureX, - textureY, - }) - wallColor = shadeColor(wallColor, shade) - data[y * stride + x + bounds.Min.X] = wallColor - - default: - data[y * stride + x + bounds.Min.X] = floorColor - } - } - - // increment angle - ray.Angle += element.Camera.Fov / float64(width) - } - - // element.drawMinimap() -} - func shadeColor (c color.RGBA, brightness float64) color.RGBA { return color.RGBA { uint8(float64(c.R) * brightness), @@ -187,8 +193,8 @@ func shadeColor (c color.RGBA, brightness float64) color.RGBA { } } -func (element *Raycaster) drawMinimap () { - bounds := element.Bounds() +func (element *Raycaster) drawMinimap (destination canvas.Canvas) { + bounds := element.entity.Bounds() scale := 8 for y := 0; y < len(element.world.Data) / element.world.Stride; y ++ { for x := 0; x < element.world.Stride; x ++ { @@ -204,7 +210,7 @@ func (element *Raycaster) drawMinimap () { cellColor = color.RGBA { 0xFF, 0xFF, 0xFF, 0xFF } } shapes.FillColorRectangle ( - element.core, + destination, cellColor, cellBounds.Inset(1)) }} @@ -219,16 +225,16 @@ func (element *Raycaster) drawMinimap () { playerBounds := image.Rectangle { playerPt, playerPt }.Inset(scale / -8) shapes.FillColorEllipse ( - element.core, + destination, artist.Hex(0xFFFFFFFF), playerBounds) shapes.ColorLine ( - element.core, + destination, artist.Hex(0xFFFFFFFF), 1, playerPt, playerAnglePt) shapes.ColorLine ( - element.core, + destination, artist.Hex(0x00FF00FF), 1, playerPt, hitPt) diff --git a/examples/scroll/main.go b/examples/scroll/main.go index 2436bd9..05cbbdf 100644 --- a/examples/scroll/main.go +++ b/examples/scroll/main.go @@ -2,10 +2,8 @@ package main import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -14,37 +12,39 @@ func main () { func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 360, 240)) window.SetTitle("Scroll") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) textBox := elements.NewTextBox("", copypasta) - scrollContainer := containers.NewScrollContainer(true, false) - disconnectedContainer := containers.NewContainer (layouts.Horizontal { - Gap: true, - }) + disconnectedContainer := elements.NewHBox(elements.SpaceMargin) list := elements.NewList ( - elements.NewListEntry("This is list item 0", nil), - elements.NewListEntry("This is list item 1", nil), - elements.NewListEntry("This is list item 2", nil), - elements.NewListEntry("This is list item 3", nil), - elements.NewListEntry("This is list item 4", nil), - elements.NewListEntry("This is list item 5", nil), - elements.NewListEntry("This is list item 6", nil), - elements.NewListEntry("This is list item 7", nil), - elements.NewListEntry("This is list item 8", nil), - elements.NewListEntry("This is list item 9", nil), - elements.NewListEntry("This is list item 10", nil), - elements.NewListEntry("This is list item 11", nil), - elements.NewListEntry("This is list item 12", nil), - elements.NewListEntry("This is list item 13", nil), - elements.NewListEntry("This is list item 14", nil), - elements.NewListEntry("This is list item 15", nil), - elements.NewListEntry("This is list item 16", nil), - elements.NewListEntry("This is list item 17", nil), - elements.NewListEntry("This is list item 18", nil), - elements.NewListEntry("This is list item 19", nil), - elements.NewListEntry("This is list item 20", nil)) + 2, + elements.NewCell(elements.NewCheckbox("Item 0", true)), + elements.NewCell(elements.NewCheckbox("Item 1", false)), + elements.NewCell(elements.NewCheckbox("Item 2", false)), + elements.NewCell(elements.NewCheckbox("Item 3", true)), + elements.NewCell(elements.NewCheckbox("Item 4", false)), + elements.NewCell(elements.NewCheckbox("Item 5", false)), + elements.NewCell(elements.NewCheckbox("Item 6", false)), + elements.NewCell(elements.NewCheckbox("Item 7", true)), + elements.NewCell(elements.NewCheckbox("Item 8", true)), + elements.NewCell(elements.NewCheckbox("Item 9", false)), + elements.NewCell(elements.NewCheckbox("Item 10", false)), + elements.NewCell(elements.NewCheckbox("Item 11", true)), + elements.NewCell(elements.NewCheckbox("Item 12", false)), + elements.NewCell(elements.NewCheckbox("Item 13", true)), + elements.NewCell(elements.NewCheckbox("Item 14", false)), + elements.NewCell(elements.NewCheckbox("Item 15", false)), + elements.NewCell(elements.NewCheckbox("Item 16", true)), + elements.NewCell(elements.NewCheckbox("Item 17", true)), + elements.NewCell(elements.NewCheckbox("Item 18", false)), + elements.NewCell(elements.NewCheckbox("Item 19", false)), + elements.NewCell(elements.NewCheckbox("Item 20", true)), + elements.NewCell(elements.NewCheckbox("Item 21", false)), + elements.NewCell(elements.NewScroll ( + elements.ScrollHorizontal, + elements.NewTextBox("", "I bet you weren't expecting this!")))) list.Collapse(0, 32) scrollBar := elements.NewScrollBar(true) list.OnScrollBoundsChange (func () { @@ -56,17 +56,16 @@ func run () { list.ScrollTo(viewport) }) - scrollContainer.Adopt(textBox) - container.Adopt(elements.NewLabel("A ScrollContainer:", false), false) - container.Adopt(scrollContainer, false) - disconnectedContainer.Adopt(list, false) - disconnectedContainer.Adopt (elements.NewLabel ( + container.Adopt(elements.NewLabel("A ScrollContainer:")) + container.Adopt(elements.NewScroll(elements.ScrollHorizontal, textBox)) + disconnectedContainer.Adopt(list) + disconnectedContainer.AdoptExpand(elements.NewLabelWrapped ( "Notice how the scroll bar to the right can be used to " + "control the list, despite not even touching it. It is " + "indeed a thing you can do. It is also terrible UI design so " + - "don't do it.", true), true) - disconnectedContainer.Adopt(scrollBar, false) - container.Adopt(disconnectedContainer, true) + "don't do it.")) + disconnectedContainer.Adopt(scrollBar) + container.AdoptExpand(disconnectedContainer) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/spacer/main.go b/examples/spacer/main.go index 92d6855..a537a37 100644 --- a/examples/spacer/main.go +++ b/examples/spacer/main.go @@ -1,10 +1,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -14,15 +12,15 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Spaced Out") - container := containers.NewContainer(layouts.Vertical { true, true }) - window.Adopt(container) - - container.Adopt (elements.NewLabel("This is at the top", false), false) - container.Adopt (elements.NewSpacer(true), false) - container.Adopt (elements.NewLabel("This is in the middle", false), false) - container.Adopt (elements.NewSpacer(false), true) - container.Adopt (elements.NewLabel("This is at the bottom", false), false) + container := elements.NewVBox ( + elements.SpaceBoth, + elements.NewLabel("This is at the top"), + elements.NewLine(), + elements.NewLabel("This is in the middle")) + container.AdoptExpand(elements.NewSpacer()) + container.Adopt(elements.NewLabel("This is at the bottom")) + window.Adopt(container) window.OnClose(tomo.Stop) window.Show() } diff --git a/examples/switch/main.go b/examples/switch/main.go index 2c7ccda..70c2b88 100644 --- a/examples/switch/main.go +++ b/examples/switch/main.go @@ -1,10 +1,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" func main () { tomo.Run(run) @@ -14,12 +12,12 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 0, 0)) window.SetTitle("Switches") - container := containers.NewContainer(layouts.Vertical { true, true }) + container := elements.NewVBox(elements.SpaceBoth) window.Adopt(container) - container.Adopt(elements.NewSwitch("hahahah", false), false) - container.Adopt(elements.NewSwitch("hehehehheheh", false), false) - container.Adopt(elements.NewSwitch("you can flick da swicth", false), false) + container.Adopt(elements.NewSwitch("hahahah", false)) + container.Adopt(elements.NewSwitch("hehehehheheh", false)) + container.Adopt(elements.NewSwitch("you can flick da swicth", false)) window.OnClose(tomo.Stop) window.Show() diff --git a/examples/test/main.go b/examples/test/main.go deleted file mode 100644 index d4c352f..0000000 --- a/examples/test/main.go +++ /dev/null @@ -1,17 +0,0 @@ -package main - -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/elements/testing" -import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" - -func main () { - tomo.Run(run) -} - -func run () { - window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128)) - window.SetTitle("hellorld!") - window.Adopt(testing.NewMouse()) - window.OnClose(tomo.Stop) - window.Show() -} diff --git a/examples/verticalLayout/main.go b/examples/vbox/main.go similarity index 57% rename from examples/verticalLayout/main.go rename to examples/vbox/main.go index ddced0a..3c6bfd9 100644 --- a/examples/verticalLayout/main.go +++ b/examples/vbox/main.go @@ -1,10 +1,8 @@ package main import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" import "git.tebibyte.media/sashakoshka/tomo/elements/testing" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" import _ "git.tebibyte.media/sashakoshka/tomo/backends/all" func main () { @@ -15,26 +13,25 @@ func run () { window, _ := tomo.NewWindow(tomo.Bounds(0, 0, 128, 128)) window.SetTitle("vertical stack") - container := containers.NewContainer(layouts.Vertical { true, true }) - window.Adopt(container) + container := elements.NewVBox(elements.SpaceBoth) - label := elements.NewLabel("it is a label hehe", true) + label := elements.NewLabelWrapped("it is a label hehe") button := elements.NewButton("drawing pad") okButton := elements.NewButton("OK") button.OnClick (func () { container.DisownAll() - container.Adopt(elements.NewLabel("Draw here:", false), false) - container.Adopt(testing.NewMouse(), true) - container.Adopt(okButton, false) + container.Adopt(elements.NewLabel("Draw here (not really):")) + container.AdoptExpand(testing.NewMouse()) + container.Adopt(okButton) okButton.Focus() }) okButton.OnClick(tomo.Stop) + + container.AdoptExpand(label) + container.Adopt(button, okButton) + window.Adopt(container) - container.Adopt(label, true) - container.Adopt(button, false) - container.Adopt(okButton, false) okButton.Focus() - window.OnClose(tomo.Stop) window.Show() } diff --git a/layout.go b/layout.go deleted file mode 100644 index 0eff659..0000000 --- a/layout.go +++ /dev/null @@ -1,37 +0,0 @@ -package tomo - -import "image" -import "git.tebibyte.media/sashakoshka/tomo/artist" - -// LayoutEntry associates an element with layout and positioning information so -// it can be arranged by a Layout. -type LayoutEntry struct { - Element - Bounds image.Rectangle - Expand bool -} - -// Layout is capable of arranging elements within a container. It is also able -// to determine the minimum amount of room it needs to do so. -type Layout interface { - // Arrange takes in a slice of entries and a bounding width and height, - // and changes the position of the entiries in the slice so that they - // are properly laid out. The given width and height should not be less - // than what is returned by MinimumSize. - Arrange ( - entries []LayoutEntry, - margin image.Point, - padding artist.Inset, - bounds image.Rectangle, - ) - - // MinimumSize returns the minimum width and height that the layout - // needs to properly arrange the given slice of layout entries. - MinimumSize ( - entries []LayoutEntry, - margin image.Point, - padding artist.Inset, - ) ( - width, height int, - ) -} diff --git a/layouts/dialog.go b/layouts/dialog.go deleted file mode 100644 index bf61e99..0000000 --- a/layouts/dialog.go +++ /dev/null @@ -1,152 +0,0 @@ -package layouts - -import "image" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/artist" - -// Dialog arranges elements in the form of a dialog box. The first element is -// positioned above as the main focus of the dialog, and is set to expand -// regardless of whether it is expanding or not. The remaining elements are -// arranged at the bottom in a row called the control row, which is aligned to -// the right, the last element being the rightmost one. -type Dialog struct { - // If Mergin is true, a margin will be placed between each element. - Gap bool - - // If Pad is true, there will be padding running along the inside of the - // layout's border. - Pad bool -} - -// Arrange arranges a list of entries into a dialog. -func (layout Dialog) Arrange ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - bounds image.Rectangle, -) { - if layout.Pad { bounds = padding.Apply(bounds) } - - controlRowWidth, controlRowHeight := 0, 0 - if len(entries) > 1 { - controlRowWidth, - controlRowHeight = layout.minimumSizeOfControlRow ( - entries[1:], margin, padding) - } - - if len(entries) > 0 { - main := entries[0] - main.Bounds.Min = bounds.Min - mainHeight := bounds.Dy() - controlRowHeight - if layout.Gap { - mainHeight -= margin.Y - } - main.Bounds.Max = main.Bounds.Min.Add(image.Pt(bounds.Dx(), mainHeight)) - entries[0] = main - } - - if len(entries) > 1 { - freeSpace := bounds.Dx() - expandingElements := 0 - - // count the number of expanding elements and the amount of free - // space for them to collectively occupy - for index, entry := range entries[1:] { - if entry.Expand { - expandingElements ++ - } else { - entryMinWidth, _ := entry.MinimumSize() - freeSpace -= entryMinWidth - } - if index > 0 && layout.Gap { - freeSpace -= margin.X - } - } - expandingElementWidth := 0 - if expandingElements > 0 { - expandingElementWidth = freeSpace / expandingElements - } - - // determine starting position and dimensions for control row - dot := image.Pt(bounds.Min.X, bounds.Max.Y - controlRowHeight) - if expandingElements == 0 { - dot.X = bounds.Max.X - controlRowWidth - } - - // set the size and position of each element in the control row - for index, entry := range entries[1:] { - if index > 0 && layout.Gap { dot.X += margin.X } - - entry.Bounds.Min = dot - entryWidth := 0 - if entry.Expand { - entryWidth = expandingElementWidth - } else { - entryWidth, _ = entry.MinimumSize() - } - dot.X += entryWidth - entryBounds := entry.Bounds - if entryBounds.Dy() != controlRowHeight || - entryBounds.Dx() != entryWidth { - entry.Bounds.Max = entryBounds.Min.Add ( - image.Pt(entryWidth, controlRowHeight)) - } - entries[index + 1] = entry - } - } - - -} - -// MinimumSize returns the minimum width and height that will be needed to -// arrange the given list of entries. -func (layout Dialog) MinimumSize ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, -) ( - width, height int, -) { - if len(entries) > 0 { - mainChildHeight := 0 - width, mainChildHeight = entries[0].MinimumSize() - height += mainChildHeight - } - - if len(entries) > 1 { - if layout.Gap { height += margin.X } - additionalWidth, - additionalHeight := layout.minimumSizeOfControlRow ( - entries[1:], margin, padding) - height += additionalHeight - if additionalWidth > width { - width = additionalWidth - } - } - - if layout.Pad { - width += padding.Horizontal() - height += padding.Vertical() - } - return -} - -func (layout Dialog) minimumSizeOfControlRow ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, -) ( - width, height int, -) { - for index, entry := range entries { - entryWidth, entryHeight := entry.MinimumSize() - if entryHeight > height { - height = entryHeight - } - width += entryWidth - if layout.Gap && index > 0 { - width += margin.X - } - } - return -} diff --git a/layouts/doc.go b/layouts/doc.go deleted file mode 100644 index bd7126c..0000000 --- a/layouts/doc.go +++ /dev/null @@ -1,2 +0,0 @@ -// Package layouts provides a set of pre-made layouts. -package layouts diff --git a/layouts/horizontal.go b/layouts/horizontal.go deleted file mode 100644 index f36f7a4..0000000 --- a/layouts/horizontal.go +++ /dev/null @@ -1,110 +0,0 @@ -package layouts - -import "image" -import "golang.org/x/image/math/fixed" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/fixedutil" - -// Horizontal arranges elements horizontally. Elements at the start of the entry -// list will be positioned on the left, and elements at the end of the entry -// list will positioned on the right. All elements have the same height. -type Horizontal struct { - // If Gap is true, a gap will be placed between each element. - Gap bool - - // If Pad is true, there will be padding running along the inside of the - // layout's border. - Pad bool -} - -// Arrange arranges a list of entries horizontally. -func (layout Horizontal) Arrange ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - bounds image.Rectangle, -) { - if layout.Pad { bounds = padding.Apply(bounds) } - - // get width of expanding elements - expandingElementWidth := layout.expandingElementWidth ( - entries, margin, padding, bounds.Dx()) - - // set the size and position of each element - dot := fixedutil.Pt(bounds.Min) - for index, entry := range entries { - if index > 0 && layout.Gap { dot.X += fixed.I(margin.X) } - - entry.Bounds.Min = fixedutil.FloorPt(dot) - entryWidth := fixed.Int26_6(0) - if entry.Expand { - entryWidth = expandingElementWidth - } else { - min, _ := entry.MinimumSize() - entryWidth = fixed.I(min) - } - dot.X += entryWidth - entry.Bounds.Max = entry.Bounds.Min.Add ( - image.Pt(entryWidth.Floor(), bounds.Dy())) - - entries[index] = entry - } -} - -// MinimumSize returns the minimum width and height that will be needed to -// arrange the given list of entries. -func (layout Horizontal) MinimumSize ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, -) ( - width, height int, -) { - for index, entry := range entries { - entryWidth, entryHeight := entry.MinimumSize() - if entryHeight > height { - height = entryHeight - } - width += entryWidth - if layout.Gap && index > 0 { - width += margin.X - } - } - - if layout.Pad { - width += padding.Horizontal() - height += padding.Vertical() - } - return -} - -func (layout Horizontal) expandingElementWidth ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - freeSpace int, -) ( - width fixed.Int26_6, -) { - expandingElements := 0 - - // count the number of expanding elements and the amount of free space - // for them to collectively occupy - for index, entry := range entries { - if entry.Expand { - expandingElements ++ - } else { - entryMinWidth, _ := entry.MinimumSize() - freeSpace -= entryMinWidth - } - if index > 0 && layout.Gap { - freeSpace -= margin.X - } - } - - if expandingElements > 0 { - width = fixed.I(freeSpace) / fixed.Int26_6(expandingElements) - } - return -} diff --git a/layouts/vertical.go b/layouts/vertical.go deleted file mode 100644 index c3daa4b..0000000 --- a/layouts/vertical.go +++ /dev/null @@ -1,113 +0,0 @@ -package layouts - -import "image" -import "golang.org/x/image/math/fixed" -import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/artist" -import "git.tebibyte.media/sashakoshka/tomo/fixedutil" - -// Vertical arranges elements vertically. Elements at the start of the entry -// list will be positioned at the top, and elements at the end of the entry list -// will positioned at the bottom. All elements have the same width. -type Vertical struct { - // If Gap is true, a gap will be placed between each element. - Gap bool - - // If Pad is true, there will be padding running along the inside of the - // layout's border. - Pad bool -} - -// Arrange arranges a list of entries vertically. -func (layout Vertical) Arrange ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - bounds image.Rectangle, -) { - if layout.Pad { bounds = padding.Apply(bounds) } - - // get height of expanding elements - expandingElementHeight, minimumHeights := layout.expandingElementHeight ( - entries, margin, padding, bounds.Dy()) - - // set the size and position of each element - dot := fixedutil.Pt(bounds.Min) - for index, entry := range entries { - if index > 0 && layout.Gap { dot.Y += fixed.I(margin.Y) } - - entry.Bounds.Min = fixedutil.FloorPt(dot) - entryHeight := fixed.Int26_6(0) - if entry.Expand { - entryHeight = expandingElementHeight - } else { - entryHeight = fixed.I(minimumHeights[index]) - } - dot.Y += entryHeight - entryBounds := entry.Bounds - entry.Bounds.Max = entryBounds.Min.Add ( - image.Pt(bounds.Dx(), - entryHeight.Floor())) - entries[index] = entry - } -} - -// MinimumSize returns the minimum width and height that will be needed to -// arrange the given list of entries. -func (layout Vertical) MinimumSize ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, -) ( - width, height int, -) { - for index, entry := range entries { - entryWidth, entryHeight := entry.MinimumSize() - if entryWidth > width { - width = entryWidth - } - height += entryHeight - if layout.Gap && index > 0 { - height += margin.Y - } - } - - if layout.Pad { - width += padding.Horizontal() - height += padding.Vertical() - } - return -} - -func (layout Vertical) expandingElementHeight ( - entries []tomo.LayoutEntry, - margin image.Point, - padding artist.Inset, - freeSpace int, -) ( - height fixed.Int26_6, - minimumHeights []int, -) { - // count the number of expanding elements and the amount of free space - // for them to collectively occupy, while gathering minimum heights. - minimumHeights = make([]int, len(entries)) - expandingElements := 0 - for index, entry := range entries { - _, entryMinHeight := entry.MinimumSize() - minimumHeights[index] = entryMinHeight - - if entry.Expand { - expandingElements ++ - } else { - freeSpace -= entryMinHeight - } - if index > 0 && layout.Gap { - freeSpace -= margin.Y - } - } - - if expandingElements > 0 { - height = fixed.I(freeSpace) / fixed.Int26_6(expandingElements) - } - return -} diff --git a/parent.go b/parent.go deleted file mode 100644 index 57aa45d..0000000 --- a/parent.go +++ /dev/null @@ -1,71 +0,0 @@ -package tomo - -import "image" - -// Parent represents a type capable of containing child elements. -type Parent interface { - // NotifyMinimumSizeChange notifies the container that a child element's - // minimum size has changed. This method is expected to be called by - // child elements when their minimum size changes. - NotifyMinimumSizeChange (child Element) - - // Window returns the window containing the parent. - Window () Window -} - -// FocusableParent represents a parent with keyboard navigation support. -type FocusableParent interface { - Parent - - // RequestFocus notifies the parent that a child element is requesting - // keyboard focus. If the parent grants the request, the method will - // return true and the child element should behave as if a HandleFocus - // call was made. - RequestFocus (child Focusable) (granted bool) - - // RequestFocusMotion notifies the parent that a child element wants the - // focus to be moved to the next focusable element. - RequestFocusNext (child Focusable) - - // RequestFocusMotion notifies the parent that a child element wants the - // focus to be moved to the previous focusable element. - RequestFocusPrevious (child Focusable) -} - -// FlexibleParent represents a parent that accounts for elements with -// flexible height. -type FlexibleParent interface { - Parent - - // NotifyFlexibleHeightChange notifies the parent that the parameters - // affecting a child's flexible height have changed. This method is - // expected to be called by flexible child element when their content - // changes. - NotifyFlexibleHeightChange (child Flexible) -} - -// ScrollableParent represents a parent that can change the scroll -// position of its child element(s). -type ScrollableParent interface { - Parent - - // NotifyScrollBoundsChange notifies the parent that a child's scroll - // content bounds or viewport bounds have changed. This is expected to - // be called by child elements when they change their supported scroll - // axes, their scroll position (either autonomously or as a result of a - // call to ScrollTo()), or their content size. - NotifyScrollBoundsChange (child Scrollable) -} - -// BackgroundParent represents a parent that is able to re-draw a portion of its -// background upon request. This is intended to be used by transparent elements -// that want to adopt their parent's background pattern. If a parent implements -// this interface, it should call a child's DrawTo method when its area of the -// background is affected. -type BackgroundParent interface { - Parent - - // DrawBackground draws a portion of the parent's background pattern - // within the specified bounds. The parent will not push these changes. - DrawBackground (bounds image.Rectangle) -} diff --git a/popups/dialog.go b/popups/dialog.go index 8339c2c..722cc6f 100644 --- a/popups/dialog.go +++ b/popups/dialog.go @@ -2,9 +2,7 @@ package popups import "image" import "git.tebibyte.media/sashakoshka/tomo" -import "git.tebibyte.media/sashakoshka/tomo/layouts" import "git.tebibyte.media/sashakoshka/tomo/elements" -import "git.tebibyte.media/sashakoshka/tomo/elements/containers" // DialogKind defines the semantic role of a dialog window. type DialogKind int @@ -16,6 +14,8 @@ const ( DialogKindError ) +// TODO: add ability to have an icon for buttons + // Button represents a dialog response button. type Button struct { // Name contains the text to display on the button. @@ -42,11 +42,11 @@ func NewDialog ( window, _ = parent.NewModal(image.Rectangle { }) } window.SetTitle(title) + + box := elements.NewVBox(elements.SpaceBoth) + messageRow := elements.NewHBox(elements.SpaceMargin) + controlRow := elements.NewHBox(elements.SpaceMargin) - container := containers.NewContainer(layouts.Dialog { true, true }) - window.Adopt(container) - - messageContainer := containers.NewContainer(layouts.Horizontal { true, false }) iconId := tomo.IconInformation switch kind { case DialogKindInfo: iconId = tomo.IconInformation @@ -55,15 +55,19 @@ func NewDialog ( case DialogKindError: iconId = tomo.IconError } - messageContainer.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge), false) - messageContainer.Adopt(elements.NewLabel(message, false), true) - container.Adopt(messageContainer, true) + messageRow.Adopt(elements.NewIcon(iconId, tomo.IconSizeLarge)) + messageRow.AdoptExpand(elements.NewLabel(message)) + + controlRow.AdoptExpand(elements.NewSpacer()) + box.AdoptExpand(messageRow) + box.Adopt(controlRow) + window.Adopt(box) if len(buttons) == 0 { button := elements.NewButton("OK") button.SetIcon(tomo.IconYes) button.OnClick(window.Close) - container.Adopt(button, false) + controlRow.Adopt(button) button.Focus() } else { var button *elements.Button @@ -74,7 +78,7 @@ func NewDialog ( buttonDescriptor.OnPress() window.Close() }) - container.Adopt(button, false) + controlRow.Adopt(button) } button.Focus() } diff --git a/theme.go b/theme.go index 73af8b8..15a651c 100644 --- a/theme.go +++ b/theme.go @@ -197,8 +197,8 @@ const ( IconBackward IconForward - IconRefresh IconUpward + IconRefresh IconYes IconNo diff --git a/tomo.go b/tomo.go index 28a57a3..a8763ca 100644 --- a/tomo.go +++ b/tomo.go @@ -29,6 +29,12 @@ func Do (callback func ()) { backend.Do(callback) } +// NewEntity generates an entity for an element using the current backend. +func NewEntity (owner Element) Entity { + assertBackend() + return backend.NewEntity(owner) +} + // NewWindow creates a new window using the current backend, and returns it as a // MainWindow. If the window could not be created, an error is returned // explaining why. diff --git a/window.go b/window.go index 1813a81..a88b2c8 100644 --- a/window.go +++ b/window.go @@ -15,9 +15,6 @@ type Window interface { // these at one time. Adopt (Element) - // Child returns the root element of the window. - Child () Element - // SetTitle sets the title that appears on the window's title bar. This // method might have no effect with some backends. SetTitle (string)