init layout, notifications, tabs & more

Signed-off-by: Hadi <112569860+anotherhadi@users.noreply.github.com>
This commit is contained in:
Hadi
2026-08-17 22:09:32 +02:00
parent f3a1877832
commit 5f99fa7521
68 changed files with 6375 additions and 330 deletions
+210
View File
@@ -0,0 +1,210 @@
package layout
// Direction is the axis a Split divides its space along.
type Direction int
const (
// Horizontal places the first child on the left, the second on the right.
Horizontal Direction = iota
// Vertical stacks the first child on top of the second.
Vertical
)
// Node is one slot in the layout tree: either a Leaf (holding a Pane) or a
// Split (dividing its space between two child Nodes).
// Build a tree with Leaf and Split/HSplit/VSplit, then hand the root to New.
//
// Node is pointer-based on purpose: panes need a stable identity across
// arbitrarily nested splits, so there's no flat index to keep in sync the
// way a slice-based component would.
type Node struct {
id string // "" means unaddressable; always non-empty for a Leaf
leaf bool
model Pane // set when leaf
dir Direction // set when !leaf
ratio float64 // proportion of space given to first; set when !leaf
min, max int // clamp on first's resolved cell size, in cells; 0 = unset
first, second *Node // set when !leaf
}
// Leaf wraps a single pane. id must be non-empty and unique within the tree
// it ends up in: it's how the pane is targeted later by SendMsg,
// RequestFocusMsg, SplitLeaf, CloseLeaf and Resize.
func Leaf(id string, model Pane) *Node {
return &Node{id: id, leaf: true, model: model}
}
// Split divides its space between first and second along dir, giving ratio
// (0 to 1) of it to first and the rest to second. id may be "" if the split
// itself never needs to be addressed by Resize; it plays no role in pane
// addressing (only Leaf ids do).
func Split(id string, dir Direction, ratio float64, first, second *Node) *Node {
return &Node{id: id, dir: dir, ratio: ratio, first: first, second: second}
}
// HSplit is Split with Horizontal and no id: layout.HSplit(0.3, left, right).
func HSplit(ratio float64, first, second *Node) *Node {
return Split("", Horizontal, ratio, first, second)
}
// VSplit is Split with Vertical and no id: layout.VSplit(0.3, top, bottom).
func VSplit(ratio float64, first, second *Node) *Node {
return Split("", Vertical, ratio, first, second)
}
// WithID sets the id used to address this node later (currently only
// meaningful on a Split, for Resize; a Leaf already gets its id from Leaf).
func (n *Node) WithID(id string) *Node {
n.id = id
return n
}
// WithMinimum clamps first's resolved size to never go below cells. No-op on
// a Leaf, which has no size of its own to constrain.
func (n *Node) WithMinimum(cells int) *Node {
if !n.leaf {
n.min = cells
}
return n
}
// WithMaximum clamps first's resolved size to never exceed cells. No-op on a
// Leaf. Setting both WithMinimum and WithMaximum to the same value fixes
// first's size regardless of ratio.
func (n *Node) WithMaximum(cells int) *Node {
if !n.leaf {
n.max = cells
}
return n
}
// findNode returns the node with the given id anywhere in the tree rooted at
// n, without mutating anything. id == "" never matches (it's the
// unaddressable sentinel, and several Splits may share it).
func findNode(n *Node, id string) (*Node, bool) {
if n == nil || id == "" {
return nil, false
}
if n.id == id {
return n, true
}
if n.leaf {
return nil, false
}
if found, ok := findNode(n.first, id); ok {
return found, true
}
return findNode(n.second, id)
}
// splitConfig collects SplitOption values applied by SplitLeaf.
type splitConfig struct {
id string
ratio float64
}
// SplitOption configures the Split node SplitLeaf creates in place of the
// leaf it splits.
type SplitOption func(*Node, *splitConfig)
// WithSplitID addresses the split SplitLeaf creates, so it can later be
// targeted by Resize.
func WithSplitID(id string) SplitOption {
return func(_ *Node, c *splitConfig) { c.id = id }
}
// WithSplitRatio overrides SplitLeaf's default 50/50 split.
func WithSplitRatio(ratio float64) SplitOption {
return func(_ *Node, c *splitConfig) { c.ratio = ratio }
}
// WithSplitMinimum clamps the size of the original (pre-split) leaf's side
// of the new split. Equivalent to calling (*Node).WithMinimum on the split
// SplitLeaf produces.
func WithSplitMinimum(cells int) SplitOption {
return func(n *Node, _ *splitConfig) { n.WithMinimum(cells) }
}
// WithSplitMaximum clamps the size of the original (pre-split) leaf's side
// of the new split. Equivalent to calling (*Node).WithMaximum on the split
// SplitLeaf produces.
func WithSplitMaximum(cells int) SplitOption {
return func(n *Node, _ *splitConfig) { n.WithMaximum(cells) }
}
// splitLeaf replaces the leaf identified by id with a Split holding the
// original leaf as first and a new Leaf(newID, newModel) as second. Returns
// the (possibly new) tree root and whether id was found and was a leaf.
func splitLeaf(root *Node, id string, dir Direction, newID string, newModel Pane, opts ...SplitOption) (*Node, bool) {
target, ok := findNode(root, id)
if !ok || !target.leaf {
return root, false
}
cfg := splitConfig{ratio: 0.5}
split := Split("", dir, 0.5, target, Leaf(newID, newModel))
for _, opt := range opts {
opt(split, &cfg)
}
split.id = cfg.id
split.ratio = cfg.ratio
newRoot, _ := replaceNode(root, id, func(*Node) *Node { return split })
return newRoot, true
}
// replaceNode returns a copy of the tree rooted at n with the node
// identified by id swapped for transform's result. Only the path down to
// that node is cloned, everything else is shared. Returns n unchanged and
// false if id isn't found.
func replaceNode(n *Node, id string, transform func(*Node) *Node) (*Node, bool) {
if n == nil || id == "" {
return n, false
}
if n.id == id {
return transform(n), true
}
if n.leaf {
return n, false
}
if newFirst, ok := replaceNode(n.first, id, transform); ok {
clone := *n
clone.first = newFirst
return &clone, true
}
if newSecond, ok := replaceNode(n.second, id, transform); ok {
clone := *n
clone.second = newSecond
return &clone, true
}
return n, false
}
// closeLeaf removes the leaf identified by id, promoting its sibling to take
// the place of their parent Split. Returns the (possibly new) tree root and
// whether id was found as a direct child of some Split (the tree's own root
// leaf, with no parent, can never be closed this way).
func closeLeaf(root *Node, id string) (*Node, bool) {
if root == nil || root.leaf || id == "" {
return root, false
}
if root.first.leaf && root.first.id == id {
return root.second, true
}
if root.second.leaf && root.second.id == id {
return root.first, true
}
if newFirst, ok := closeLeaf(root.first, id); ok {
clone := *root
clone.first = newFirst
return &clone, true
}
if newSecond, ok := closeLeaf(root.second, id); ok {
clone := *root
clone.second = newSecond
return &clone, true
}
return root, false
}