cchat-gtk/internal/ui/service/session/server/traverse/traverse.go

182 lines
5.1 KiB
Go

// Package traverse implements an extensible interface that allows children
// widgets to announce state changes to their parent container.
//
// The objective of this package is to allow for easier parent traversal without
// cluttering its structure with too much state. It also allows for proper
// encapsulation as well as a fallback mechanism without lengthy boolean checks.
package traverse
import (
"github.com/diamondburned/cchat"
)
// Breadcrumber is the base interface that other interfaces extend on. A child
// must at minimum implement this interface to use any other.
type Breadcrumber interface {
// Breadcrumb returns the parent's path before the children's breadcrumb.
// This method recursively joins the parent's crumb with the children's,
// then eventually make its way up to the root node.
ParentBreadcrumb() Breadcrumber
}
type BreadcrumbNamer interface {
// Breadcrumb returns the breadcrumb name.
Breadcrumb() string
// TODO: make BreadcrumbNamer return LabelState.
}
// Traverse traverses the given breadcrumber recursively. If traverser returns
// true, then the function halts. Traversal is done from parent down to
// children.
func Traverse(bc Breadcrumber, traverser func(b Breadcrumber) bool) {
if bc == nil {
return
}
var stack []Breadcrumber
for current := bc; current != nil; current = current.ParentBreadcrumb() {
stack = append(stack, current)
}
for _, bc := range stack {
if traverser(bc) {
return
}
}
}
// TryBreadcrumb accepts a nilable breadcrumber and handles it appropriately.
func TryBreadcrumb(i Breadcrumber) (breadcrumbs []string) {
Traverse(i, func(b Breadcrumber) bool {
if namer, ok := b.(BreadcrumbNamer); ok {
breadcrumbs = append(breadcrumbs, namer.Breadcrumb())
}
return false
})
for l, r := 0, len(breadcrumbs)-1; l < r; l, r = l+1, r-1 {
breadcrumbs[l], breadcrumbs[r] = breadcrumbs[r], breadcrumbs[l]
}
return
}
func TryID(i Breadcrumber, appended ...cchat.ID) (ids []cchat.ID) {
Traverse(i, func(b Breadcrumber) bool {
switch b := b.(type) {
case cchat.Identifier:
ids = append(ids, b.ID())
case BreadcrumbNamer:
ids = append(ids, b.Breadcrumb())
}
return false
})
for l, r := 0, len(ids)-1; l < r; l, r = l+1, r-1 {
ids[l], ids[r] = ids[r], ids[l]
}
return
}
// Unreadabler extends Breadcrumber to add unread states to the parent node.
type Unreadabler interface {
SetState(id string, unread, mentioned bool)
}
// TrySetUnread tries to check if a breadcrumber parent node supports
// Unreadabler. If it does, then this function will set the state appropriately.
func TrySetUnread(parent Breadcrumber, selfID string, unread, mentioned bool) {
if u, ok := parent.(Unreadabler); ok {
u.SetState(selfID, unread, mentioned)
}
}
// UnreadSetter is an interface that a single row implements to set state. It
// does not have to do with Breadcrumber.
type UnreadSetter interface {
SetUnreadUnsafe(unread, mentioned bool)
}
// Unreadable is a struct that nodes could embed to implement unreadable
// capability, that is, the unread and mentioned states. A zero-value Unreadable
// is a valid Unreadable without an update handler.
//
// Typically, parent nodes would implement this as a way to count the number of
// unread and mentioned children nodes.
type Unreadable struct {
UnreadableState
unreadHandler func(unread, mentioned bool)
}
func NewUnreadable(unreadHandler UnreadSetter) *Unreadable {
u := &Unreadable{}
u.SetUnreadHandler(unreadHandler.SetUnreadUnsafe)
return u
}
// SetUpdateHandler sets the parent's update handler. This update handler must
// refer to the parent's breadcrumb.
func (u *Unreadable) SetUnreadHandler(updateHandler func(unread, mentioned bool)) {
// Update with the current state.
if u.unreadHandler = updateHandler; updateHandler != nil {
updateHandler(u.State())
}
}
// SetState updates the node ID's state in this parent unreadable state
// container.
func (u *Unreadable) SetState(id string, unread, mentioned bool) {
u.UnreadableState.SetState(id, unread, mentioned)
if u.unreadHandler != nil {
u.unreadHandler(u.UnreadableState.State())
}
}
// UnreadableState implements a map of unread children for indication. A
// zero-value UnreadableState is a valid value.
type UnreadableState struct {
// both maps represent sets of server IDs
unreads map[string]struct{}
mentions map[string]struct{}
}
func NewUnreadableState() *UnreadableState {
return &UnreadableState{}
}
func (s *UnreadableState) Reset() {
s.unreads = map[string]struct{}{}
s.mentions = map[string]struct{}{}
}
func (s *UnreadableState) State() (unread, mentioned bool) {
unread = len(s.unreads) > 0
mentioned = len(s.mentions) > 0
// Count mentioned as unread.
return unread || mentioned, mentioned
}
func (s *UnreadableState) SetState(id string, unread, mentioned bool) {
if s.unreads == nil && s.mentions == nil {
s.Reset()
}
setIf(unread, id, s.unreads)
setIf(mentioned, id, s.mentions)
}
// setIf sets the ID into the given map if the cond boolean is true, or deletes
// it if the boolean is false.
func setIf(cond bool, id string, m map[string]struct{}) {
if cond {
m[id] = struct{}{}
} else {
delete(m, id)
}
}