mirror of
https://github.com/diamondburned/cchat-gtk.git
synced 2025-01-10 12:36:43 +00:00
180 lines
5 KiB
Go
180 lines
5 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
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
}
|