From 5ca3b80e8e858eaede5782e8225d21ea7ebe3df8 Mon Sep 17 00:00:00 2001 From: Sasha Koshka Date: Mon, 17 Apr 2023 02:05:53 -0400 Subject: [PATCH] Made this crazy selection system --- backends/x/entity.go | 28 +++++ backends/x/event.go | 29 ++++- backends/x/system.go | 2 +- element.go | 48 +++++++- elements/entry.go | 142 ++++++++++++++++++++++++ elements/{file => notdone}/directory.go | 0 elements/{file => notdone}/file.go | 0 elements/{file => notdone}/fs.go | 0 entity.go | 12 ++ 9 files changed, 251 insertions(+), 10 deletions(-) create mode 100644 elements/entry.go rename elements/{file => notdone}/directory.go (100%) rename elements/{file => notdone}/file.go (100%) rename elements/{file => notdone}/fs.go (100%) diff --git a/backends/x/entity.go b/backends/x/entity.go index bc9c7b0..19e37a3 100644 --- a/backends/x/entity.go +++ b/backends/x/entity.go @@ -15,6 +15,7 @@ type entity struct { minWidth int minHeight int + selected bool layoutInvalid bool isContainer bool } @@ -42,6 +43,11 @@ func (ent *entity) unlink () { } 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) { @@ -96,6 +102,14 @@ func (entity *entity) scrollTargetChildAt (point image.Point) *entity { return nil } +func (entity *entity) forMouseTargetContainers (callback func (tomo.MouseTargetContainer)) { + if entity.parent == nil { return } + if parent, ok := entity.parent.element.(tomo.MouseTargetContainer); ok { + callback(parent) + } + entity.parent.forMouseTargetContainers(callback) +} + // ----------- Entity ----------- // func (entity *entity) Invalidate () { @@ -195,6 +209,14 @@ func (entity *entity) PlaceChild (index int, bounds image.Rectangle) { child.InvalidateLayout() } +func (entity *entity) SelectChild (index int, selected bool) { + child := entity.children[index] + if element, ok := entity.element.(tomo.Selectable); ok { + child.selected = selected + element.HandleSelectionChange() + } +} + func (entity *entity) ChildMinimumSize (index int) (width, height int) { childEntity := entity.children[index] return childEntity.minWidth, childEntity.minHeight @@ -220,6 +242,12 @@ func (entity *entity) FocusPrevious () { entity.window.system.focusPrevious() } +// ----------- SelectableEntity ----------- // + +func (entity *entity) Selected () bool { + return entity.selected +} + // ----------- FlexibleEntity ----------- // func (entity *entity) NotifyFlexibleHeightChange () { diff --git a/backends/x/event.go b/backends/x/event.go index 2f573ba..84630a1 100644 --- a/backends/x/event.go +++ b/backends/x/event.go @@ -206,12 +206,19 @@ func (window *window) handleButtonPress ( } } else { underneath := window.system.childAt(point) + window.system.drags[buttonEvent.Detail] = underneath if child, ok := underneath.element.(tomo.MouseTarget); ok { - window.system.drags[buttonEvent.Detail] = child child.HandleMouseDown ( point.X, point.Y, input.Button(buttonEvent.Detail)) } + callback := func (container tomo.MouseTargetContainer) { + container.HandleChildMouseDown ( + point.X, point.Y, + input.Button(buttonEvent.Detail), + underneath.element) + } + underneath.forMouseTargetContainers(callback) } window.system.afterEvent() @@ -223,12 +230,22 @@ func (window *window) handleButtonRelease ( ) { buttonEvent := *event.ButtonReleaseEvent if buttonEvent.Detail >= 4 && buttonEvent.Detail <= 7 { return } - child := window.system.drags[buttonEvent.Detail] - if child != nil { - child.HandleMouseUp ( + 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) { + container.HandleChildMouseUp ( int(buttonEvent.EventX), int(buttonEvent.EventY), - input.Button(buttonEvent.Detail)) + input.Button(buttonEvent.Detail), + dragging.element) + } + dragging.forMouseTargetContainers(callback) } window.system.afterEvent() @@ -244,7 +261,7 @@ func (window *window) handleMotionNotify ( handled := false for _, child := range window.system.drags { - if child, ok := child.(tomo.MotionTarget); ok { + if child, ok := child.element.(tomo.MotionTarget); ok { child.HandleMotion(x, y) handled = true } diff --git a/backends/x/system.go b/backends/x/system.go index 78a43b7..540b4e8 100644 --- a/backends/x/system.go +++ b/backends/x/system.go @@ -33,7 +33,7 @@ type system struct { drawingInvalid entitySet anyLayoutInvalid bool - drags [10]tomo.MouseTarget + drags [10]*entity pushFunc func (image.Rectangle) } diff --git a/element.go b/element.go index 0b99b04..cad0a80 100644 --- a/element.go +++ b/element.go @@ -40,17 +40,43 @@ type Container interface { HandleChildMinimumSizeChange (child Element) } +// Enableable represents an element that can be enabled and disabled. Disabled +// elements typically appear greyed out. +type Enableable interface { + Element + + // Enabled returns whether or not the element is enabled. + Enabled () bool + + // SetEnabled sets whether or not the element is enabled. + SetEnabled (bool) +} + // Focusable represents an element that has keyboard navigation support. type Focusable interface { Element - - // Enabled returns whether or not the element can currently accept focus. - Enabled () bool + Enableable // HandleFocusChange is called when the element is focused or unfocused. HandleFocusChange () } +// Selectable represents an element that can be selected. This includes things +// like list items, files, etc. The difference between this and Focusable is +// that multiple Selectable elements may be selected at the same time, whereas +// only one Focusable element may be focused at the same time. Containers who's +// purpose is to contain selectable elements can determine when to select them +// by implementing MouseTargetContainer and listening for HandleChildMouseDown +// events. +type Selectable interface { + Element + Enableable + + // HandleSelectionChange is called when the element is selected or + // deselected. + HandleSelectionChange () +} + // KeyboardTarget represents an element that can receive keyboard input. type KeyboardTarget interface { Element @@ -80,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 diff --git a/elements/entry.go b/elements/entry.go new file mode 100644 index 0000000..61334c3 --- /dev/null +++ b/elements/entry.go @@ -0,0 +1,142 @@ +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 + padding bool + theme theme.Wrapped +} + +// 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, padding bool) (element *Cell) { + element = &Cell { padding: padding } + 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 if element.padding { + 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() + if element.padding { + 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.entity.InvalidateLayout() +} + +// 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() + if child, ok := element.child.(tomo.Enableable); ok { + child.SetEnabled(enabled) + } +} + +// 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.entity.InvalidateLayout() +} + +func (element *Cell) HandleSelectionChange () { + element.entity.Invalidate() +} + +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 + } + if element.padding { + padding := element.theme.Padding(tomo.PatternTableCell) + width += padding.Horizontal() + height += padding.Vertical() + } + + element.entity.SetMinimumSize(width, height) +} diff --git a/elements/file/directory.go b/elements/notdone/directory.go similarity index 100% rename from elements/file/directory.go rename to elements/notdone/directory.go diff --git a/elements/file/file.go b/elements/notdone/file.go similarity index 100% rename from elements/file/file.go rename to elements/notdone/file.go diff --git a/elements/file/fs.go b/elements/notdone/fs.go similarity index 100% rename from elements/file/fs.go rename to elements/notdone/fs.go diff --git a/entity.go b/entity.go index dfdede7..bdeb562 100644 --- a/entity.go +++ b/entity.go @@ -69,6 +69,10 @@ type ContainerEntity interface { // 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) @@ -94,6 +98,14 @@ type FocusableEntity interface { 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