Git: components 301-320
This commit is contained in:
121 files changed
+7031
-2
No files matched your search
+204
@@ -0,0 +1,204 @@
|
||||
package mujica
|
||||
|
||||
import (
|
||||
"log/slog"
|
||||
|
||||
"github.com/egoist/mygo/ui"
|
||||
|
||||
"mujicaui/internal"
|
||||
"mujicaui/theme"
|
||||
)
|
||||
|
||||
// CommitGraphState is a CommitGraph's selection and its lane layout.
|
||||
type CommitGraphState struct {
|
||||
List ListState[string]
|
||||
rows []commitGraphRow
|
||||
// lanes is the widest the graph gets.
|
||||
lanes int
|
||||
src *Commit
|
||||
n int
|
||||
}
|
||||
|
||||
// CommitGraphOptions configure a CommitGraph.
|
||||
type CommitGraphOptions struct {
|
||||
// TimeFormat lays out commit times; empty means "2006-01-02 15:04".
|
||||
TimeFormat string
|
||||
// MaxLanes caps the graph width in lanes; zero means 8.
|
||||
MaxLanes int
|
||||
Status DataStatus
|
||||
Error string
|
||||
}
|
||||
|
||||
// commitGraphLink joins lane from to lane to within a row.
|
||||
type commitGraphLink struct{ from, to, color int }
|
||||
|
||||
type commitGraphRow struct {
|
||||
node, color int
|
||||
merge bool
|
||||
// top links enter from above; bottom links leave the node.
|
||||
top, bottom []commitGraphLink
|
||||
}
|
||||
|
||||
type commitGraphLane struct {
|
||||
hash string
|
||||
color int
|
||||
}
|
||||
|
||||
// CommitGraph shows commits with branch lanes; give it a height.
|
||||
func CommitGraph(c *ui.Context, s *CommitGraphState, commits []Commit, opts CommitGraphOptions) GitListResult {
|
||||
maxLanes := opts.MaxLanes
|
||||
if maxLanes == 0 {
|
||||
maxLanes = 8
|
||||
}
|
||||
if maxLanes < 0 {
|
||||
panic("mujica: CommitGraph MaxLanes is negative")
|
||||
}
|
||||
var first *Commit
|
||||
if len(commits) > 0 {
|
||||
first = &commits[0]
|
||||
}
|
||||
if first != s.src || len(commits) != s.n {
|
||||
s.rows, s.lanes = commitGraphLayout(commits)
|
||||
s.src, s.n = first, len(commits)
|
||||
slog.Debug("mujica: commit graph layout", "commits", len(commits), "lanes", s.lanes)
|
||||
}
|
||||
layout := gitTimeLayout(opts.TimeFormat)
|
||||
lane := Density(c).Unit() * 4
|
||||
width := float32(min(max(s.lanes, 1), maxLanes)) * lane
|
||||
h := Density(c).ControlHeight()
|
||||
v := List(c, &s.List, len(commits), ListOptions[string]{
|
||||
Key: func(i int) string { return gitCommitHash(commits[i]) },
|
||||
Label: func(i int) string { return commits[i].Subject },
|
||||
Status: opts.Status, Error: opts.Error, Empty: msg(c, "commit_list.empty"),
|
||||
}, func(i int) {
|
||||
row := s.rows[i]
|
||||
ui.Box(c).Size(width, h).Shrink(0).Clip().Role(ui.RoleNone).Draw(func(p *ui.Painter, r ui.Rect) {
|
||||
commitGraphDraw(p, Tokens(c), r, row, lane)
|
||||
})
|
||||
commitListRow(c, commits[i], layout)
|
||||
})
|
||||
res := GitListResult{Element: v.Element, changed: v.Changed(), submitted: v.Submitted()}
|
||||
if res.changed || res.submitted {
|
||||
key, _ := s.List.Selected()
|
||||
slog.Debug("mujica: commit graph choose", "hash", key, "open", res.submitted)
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
// commitGraphLayout gives each commit a lane and parent links.
|
||||
func commitGraphLayout(commits []Commit) ([]commitGraphRow, int) {
|
||||
rows := make([]commitGraphRow, len(commits))
|
||||
var lanes []commitGraphLane
|
||||
colors, widest := 0, 0
|
||||
free := func() int {
|
||||
for j, l := range lanes {
|
||||
if l.hash == "" {
|
||||
return j
|
||||
}
|
||||
}
|
||||
lanes = append(lanes, commitGraphLane{})
|
||||
return len(lanes) - 1
|
||||
}
|
||||
for i, cm := range commits {
|
||||
h := gitCommitHash(cm)
|
||||
var row commitGraphRow
|
||||
row.node = -1
|
||||
for j, l := range lanes {
|
||||
if l.hash == h && row.node < 0 {
|
||||
row.node = j
|
||||
}
|
||||
}
|
||||
for j, l := range lanes {
|
||||
switch {
|
||||
case l.hash == "":
|
||||
case l.hash == h:
|
||||
row.top = append(row.top, commitGraphLink{j, row.node, l.color})
|
||||
default:
|
||||
row.top = append(row.top, commitGraphLink{j, j, l.color})
|
||||
}
|
||||
}
|
||||
if row.node < 0 {
|
||||
row.node = free()
|
||||
lanes[row.node] = commitGraphLane{h, colors % 6}
|
||||
colors++
|
||||
}
|
||||
row.color = lanes[row.node].color
|
||||
for j := range lanes {
|
||||
if lanes[j].hash == h {
|
||||
lanes[j].hash = ""
|
||||
}
|
||||
}
|
||||
row.merge = len(cm.Parents) > 1
|
||||
for pi, p := range cm.Parents {
|
||||
if pi == 0 {
|
||||
lanes[row.node] = commitGraphLane{p, row.color}
|
||||
row.bottom = append(row.bottom, commitGraphLink{row.node, row.node, row.color})
|
||||
continue
|
||||
}
|
||||
to := -1
|
||||
for j, l := range lanes {
|
||||
if l.hash == p {
|
||||
to = j
|
||||
break
|
||||
}
|
||||
}
|
||||
if to < 0 {
|
||||
to = free()
|
||||
lanes[to] = commitGraphLane{p, colors % 6}
|
||||
colors++
|
||||
}
|
||||
row.bottom = append(row.bottom, commitGraphLink{row.node, to, lanes[to].color})
|
||||
}
|
||||
for len(lanes) > 0 && lanes[len(lanes)-1].hash == "" {
|
||||
lanes = lanes[:len(lanes)-1]
|
||||
}
|
||||
widest = max(widest, len(lanes), row.node+1)
|
||||
rows[i] = row
|
||||
}
|
||||
return rows, widest
|
||||
}
|
||||
|
||||
// commitGraphColors are the lane colors; place also tells lanes apart.
|
||||
func commitGraphColors(k theme.Tokens) [6]ui.Color {
|
||||
return [6]ui.Color{k.AccentText, k.Info, k.Success, k.Warning, k.Danger, k.TextMuted}
|
||||
}
|
||||
|
||||
func commitGraphDraw(p *ui.Painter, k theme.Tokens, r ui.Rect, row commitGraphRow, lane float32) {
|
||||
cols := commitGraphColors(k)
|
||||
x := func(j int) float32 { return internal.Snap(p, r.X+lane/2+float32(j)*lane) }
|
||||
top, mid, bot := internal.Snap(p, r.Y), internal.Snap(p, r.Y+r.H/2), internal.Snap(p, r.Y+r.H)
|
||||
seg := func(l commitGraphLink, y0, y1 float32) {
|
||||
var path ui.Path
|
||||
xa, xb := x(l.from), x(l.to)
|
||||
path.MoveTo(xa, y0)
|
||||
if xa == xb {
|
||||
path.LineTo(xb, y1)
|
||||
} else {
|
||||
m := (y0 + y1) / 2
|
||||
path.CubeTo(xa, m, xb, m, xb, y1)
|
||||
}
|
||||
p.StrokePath(&path, 2, cols[l.color])
|
||||
}
|
||||
for _, l := range row.top {
|
||||
if l.from == l.to && l.to != row.node {
|
||||
seg(l, top, bot)
|
||||
continue
|
||||
}
|
||||
seg(l, top, mid)
|
||||
}
|
||||
for _, l := range row.bottom {
|
||||
seg(l, mid, bot)
|
||||
}
|
||||
cx, col := x(row.node), cols[row.color]
|
||||
if row.merge {
|
||||
p.FillPath(internal.Diamond(cx, mid, 11), k.Surface)
|
||||
p.StrokePath(internal.Diamond(cx, mid, 9), 2, col)
|
||||
return
|
||||
}
|
||||
var dot ui.Path
|
||||
dot.Circle(cx, mid, 5.5)
|
||||
p.FillPath(&dot, k.Surface)
|
||||
var ring ui.Path
|
||||
ring.Circle(cx, mid, 4)
|
||||
p.FillPath(&ring, col)
|
||||
}
|
||||
Reference in new issue
Block a user