Files
MujicaUI/git/branch_selector.go
T

215 lines
6.6 KiB
Go

package git
import (
"log/slog"
"strings"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/input"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/overlay"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// BranchSelectorOptions configure a BranchSelector.
type BranchSelectorOptions struct {
// AllowCreate offers to create a branch named by the search.
AllowCreate bool
Disabled bool
}
// BranchSelectorResult is a built BranchSelector and its events.
type BranchSelectorResult struct {
Element *ui.Element
changed bool
create string
}
// Changed reports a new branch in *selected this frame.
func (r BranchSelectorResult) Changed() bool { return r.changed }
// Created returns a branch name to create this frame.
func (r BranchSelectorResult) Created() (string, bool) { return r.create, r.create != "" }
type branchSelectorState struct {
open, wasOpen bool
query string
hl int
reveal bool
// found caches the filter of src for query fq.
found []Branch
fq string
src *Branch
n int
}
// BranchSelector chooses a branch from a searchable popup.
func BranchSelector(c *ui.Context, selected *string, branches []Branch, opts BranchSelectorOptions) BranchSelectorResult {
for _, b := range branches {
branchKey(b)
}
k := core.Tokens(c)
d := core.Density(c)
u := d.Unit()
tr := ui.ButtonBase(c).MinHeight(d.ControlHeight()).Padding(0, u*3).Gap(u*2).Radius(theme.ControlRadius).
Background(k.Surface).Border(theme.BorderWidth, k.ControlBorder).Disabled(opts.Disabled).
Label(core.Msg(c, "branch_selector.label") + ": " + *selected)
internal.Ring(tr, k.Focus, theme.ControlRadius)
if tr.Hovered() {
tr.Background(k.SurfaceHover)
}
tr.Transition(core.ColorFade)
st := ui.Local(tr, "state", func() *branchSelectorState { return &branchSelectorState{} })
s := *st
res := BranchSelectorResult{Element: tr}
if tr.Clicked() && !tr.IsDisabled() {
s.open = !s.open
}
if tr.IsDisabled() {
s.open = false
}
if s.open && !s.wasOpen {
s.query, s.hl = "", 0
slog.Debug("mujica: branch selector open")
}
s.wasOpen = s.open
tr.Children(func() {
ui.Icon(c, icons.Must("git-branch")).Size(16, 16).TextColor(k.TextMuted)
ui.Text(c, *selected).SingleLine().MaxWidth(220)
ui.Icon(c, icons.Must("chevrons-up-down")).Size(14, 14).TextColor(k.TextMuted)
})
b := tr.Bounds()
ui.PopoverBase(c, tr, &s.open, func(panel *ui.Element) {
core.OverlayStyle(c, panel, overlay.OverlayAnchored(c, panel, b))
panel.Margin(6, 0, 0, 0).Padding(u * 2).Gap(u * 2).Width(300).MaxHeight(overlay.PopoverRoom(c, b)).AlignItems(ui.Stretch)
var first *Branch
if len(branches) > 0 {
first = &branches[0]
}
if s.found == nil || s.fq != s.query || s.src != first || s.n != len(branches) {
s.found, s.fq, s.src, s.n = branchSelectorFilter(branches, s.query), s.query, first, len(branches)
}
found := s.found
create := opts.AllowCreate && strings.TrimSpace(s.query) != "" && !branchSelectorHas(branches, strings.TrimSpace(s.query))
n := len(found)
if create {
n++
}
in := input.TextInput(c, &s.query, input.TextInputOptions{Placeholder: core.Msg(c, "branch_selector.search"), Label: core.Msg(c, "branch_selector.search"),
Leading: func() { ui.Icon(c, icons.Must("search")).Size(14, 14).TextColor(k.TextMuted) }})
in.Input.AutoFocus()
if in.Changed() {
s.hl, s.reveal = 0, true
}
switch {
case n == 0:
case in.Input.Shortcut(0, ui.KeyDown):
s.hl, s.reveal = (s.hl+1)%n, true
case in.Input.Shortcut(0, ui.KeyUp):
s.hl, s.reveal = (s.hl-1+n)%n, true
}
s.hl = max(0, min(s.hl, n-1))
pick := -1
if in.Submitted() && n > 0 {
pick = s.hl
}
ui.Scroll(c).Shrink(1).Gap(2).Role(ui.RoleList).Children(func() {
if n == 0 {
ui.Text(c, core.Msg(c, "branch_selector.empty")).TextColor(k.TextMuted).Padding(u * 3)
}
group := -1
for i, br := range found {
if g := branchSelectorGroup(br); g != group {
group = g
ui.Text(c, core.Msg(c, []string{"branch_selector.local", "branch_selector.remote"}[g])).Font(theme.SerifFont).
FontSize(core.FontSize(c, theme.CaptionSize)).TextColor(k.Ornament.Mix(k.Text, 0.35)).Padding(u*2, u*2, u, u*2)
}
if branchSelectorRow(c, s, i, br.Name, br.Name == *selected, "git-branch") {
pick = i
}
}
if create && branchSelectorRow(c, s, len(found), core.Msg(c, "branch_selector.create")+" “"+strings.TrimSpace(s.query)+"”", false, "plus") {
pick = len(found)
}
})
switch {
case pick < 0:
case pick == len(found):
res.create = strings.TrimSpace(s.query)
s.open = false
slog.Debug("mujica: branch selector create", "name", res.create)
default:
s.open = false
if found[pick].Name != *selected {
*selected, res.changed = found[pick].Name, true
slog.Debug("mujica: branch selector choose", "branch", *selected)
}
}
})
return res
}
// branchSelectorRow builds row i and reports a click on it.
func branchSelectorRow(c *ui.Context, s *branchSelectorState, i int, label string, current bool, icon string) bool {
k := core.Tokens(c)
d := core.Density(c)
u := d.Unit()
row := ui.Row(c).AlignItems(ui.Center).Gap(u*2).MinHeight(d.ControlHeight()).Padding(0, u*2).Radius(theme.ControlRadius).
Role(ui.RoleListItem).Label(label).Cursor(ui.CursorPointer)
if i == s.hl {
row.Background(k.Selection)
if s.reveal {
row.ScrollIntoView()
s.reveal = false
}
} else if row.Hovered() {
row.Background(k.SurfaceHover)
}
row.Transition(core.ColorFade)
row.Children(func() {
ui.Icon(c, icons.Must(icon)).Size(14, 14).TextColor(k.TextMuted)
t := ui.Text(c, label).SingleLine().Grow(1).MinWidth(0)
if current {
t.FontWeight(600)
ui.Icon(c, icons.Must("check")).Size(14, 14).TextColor(k.AccentText)
}
})
return row.Clicked()
}
func branchSelectorGroup(b Branch) int {
if b.Remote {
return 1
}
return 0
}
// branchSelectorFilter keeps branches whose name holds the query, locals first.
func branchSelectorFilter(branches []Branch, query string) []Branch {
q := strings.ToLower(strings.TrimSpace(query))
var local, remote []Branch
for _, b := range branches {
if !strings.Contains(strings.ToLower(b.Name), q) {
continue
}
if b.Remote {
remote = append(remote, b)
} else {
local = append(local, b)
}
}
return append(append(make([]Branch, 0, len(local)+len(remote)), local...), remote...)
}
func branchSelectorHas(branches []Branch, name string) bool {
for _, b := range branches {
if b.Name == name {
return true
}
}
return false
}