Split into category packages; module github.com/ZacharyZhang-NY/MujicaUI

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 16:18:48 -04:00
1 parent 5a7e921c7e
commit 22b53e9e9e
1822 files changed
+24568 -21314

No files matched your search

+226
View File
@@ -0,0 +1,226 @@
package git
import (
"fmt"
"log/slog"
"strconv"
"github.com/egoist/mygo/ui"
"github.com/ZacharyZhang-NY/MujicaUI/core"
"github.com/ZacharyZhang-NY/MujicaUI/internal"
"github.com/ZacharyZhang-NY/MujicaUI/theme"
)
// DiffViewerState keeps a DiffViewer's diff, opened folds and scroll place.
type DiffViewerState struct {
list ui.ListState
from, to string
diff []internal.DiffLine
rows []diffViewerRow
expanded map[int]bool
split bool
context int
built bool
dirty bool
}
// DiffViewerOptions configure a DiffViewer.
type DiffViewerOptions struct {
// SideBySide shows old and new in two columns.
SideBySide bool
// Context is unchanged lines kept around changes; zero means 3.
Context int
// Language picks syntax colors, as "go"; empty is plain.
Language string
}
// diffViewerRow is a fold or one line per side.
type diffViewerRow struct {
left, right int
fold, hidden int
}
// DiffViewer compares two texts by line; give it a height.
func DiffViewer(c *ui.Context, s *DiffViewerState, from, to string, opts DiffViewerOptions) *ui.Element {
ctx := opts.Context
if ctx == 0 {
ctx = 3
}
if ctx < 0 {
panic("mujica: DiffViewer Context is negative")
}
if !s.built || s.from != from || s.to != to {
s.diff = internal.LineDiff(internal.SplitLines(from), internal.SplitLines(to))
s.from, s.to, s.built, s.dirty, s.expanded = from, to, true, true, map[int]bool{}
add, del := internal.DiffStats(s.diff)
slog.Debug("mujica: diff viewer diff", "lines", len(s.diff), "added", add, "deleted", del)
}
if s.dirty || s.split != opts.SideBySide || s.context != ctx {
s.rows = diffViewerRows(s.diff, ctx, s.expanded, opts.SideBySide)
s.split, s.context, s.dirty = opts.SideBySide, ctx, false
}
k := core.Tokens(c)
digits := float32(len(strconv.Itoa(max(len(s.diff), 1))))
numW := digits*8 + core.Density(c).Unit()*3
e := ui.List(c, &s.list, len(s.rows), func(i int) {
r := s.rows[i]
if r.hidden > 0 {
diffViewerFold(c, s, r)
return
}
row := ui.Row(c).AlignItems(ui.Stretch).MinHeight(theme.MinHitSize)
row.Children(func() {
if !opts.SideBySide {
diffViewerCell(c, s.diff[r.left], opts.Language, numW, true)
return
}
for _, side := range []int{r.left, r.right} {
half := ui.Row(c).Grow(1).Basis(0).MinWidth(0).AlignItems(ui.Stretch).Clip()
half.Children(func() {
if side < 0 {
half.Background(k.Background)
return
}
diffViewerCell(c, s.diff[side], opts.Language, numW, false)
})
}
})
})
e.Background(k.Surface).Border(theme.BorderWidth, k.Border).Radius(theme.ControlRadius).Font(theme.MonoFont)
return e
}
// diffViewerCell shows one diff line with numbers, sign and tint.
func diffViewerCell(c *ui.Context, d internal.DiffLine, language string, numW float32, both bool) {
k := core.Tokens(c)
u := core.Density(c).Unit()
small := core.FontSize(c, theme.CaptionSize)
sign, col, name := " ", k.TextMuted, ""
switch d.Op {
case internal.DiffDelete:
sign, col, name = "−", k.Danger, core.Msg(c, "diff_viewer.deleted")
case internal.DiffInsert:
sign, col, name = "+", k.Success, core.Msg(c, "diff_viewer.added")
}
cell := ui.Row(c).Grow(1).MinWidth(0).AlignItems(ui.Center)
if d.Op != internal.DiffEqual {
cell.Background(col.Mix(k.Surface, 0.88))
}
num := func(n int) {
t := ""
if n > 0 {
t = strconv.Itoa(n)
}
ui.Text(c, t).FontSize(small).FontFeatures("tnum").TextColor(k.TextMuted).Width(numW).TextAlign(ui.End).Shrink(0).Padding(0, u, 0, 0)
}
cell.Children(func() {
if both || d.Op != internal.DiffInsert {
num(d.Old)
}
if both || d.Op == internal.DiffInsert {
num(d.New)
}
ui.Text(c, sign).FontWeight(700).TextColor(col).Width(u * 4).TextAlign(ui.Center).Shrink(0)
spans, _ := internal.CodeSpans(k, language, d.Text, false)
if len(spans) == 0 || d.Text == "" {
spans = []ui.Span{{Text: " "}}
}
t := ui.RichText(c, spans...).NoWrap().MinWidth(0).Padding(u*0.5, u*2, u*0.5, 0)
if name != "" {
t.Label(name + ": " + d.Text)
}
})
}
// diffViewerFold is the button standing for hidden unchanged lines.
func diffViewerFold(c *ui.Context, s *DiffViewerState, r diffViewerRow) {
k := core.Tokens(c)
u := core.Density(c).Unit()
b := ui.ButtonBase(c).Justify(ui.Start).Gap(u*2).MinHeight(theme.MinHitSize).Padding(0, u*3).Background(k.Background).
TextColor(k.TextMuted).Cursor(ui.CursorPointer)
label := fmt.Sprintf(core.Msg(c, "diff_viewer.fold"), r.hidden)
b.Label(label)
internal.Ring(b, k.Focus, 0)
if b.Hovered() {
b.Background(k.SurfaceHover).TextColor(k.Text)
}
b.Transition(core.ColorFade)
b.Children(func() {
ui.Text(c, "⋯").FontWeight(700)
ui.Text(c, label).Font(theme.SerifFont).FontSize(core.FontSize(c, theme.CaptionSize))
})
if b.Clicked() && !b.IsDisabled() {
s.expanded[r.fold] = true
s.dirty = true
slog.Debug("mujica: diff viewer unfold", "at", r.fold, "lines", r.hidden)
}
}
// diffViewerRows folds long unchanged runs and pairs split sides.
func diffViewerRows(diff []internal.DiffLine, ctx int, expanded map[int]bool, split bool) []diffViewerRow {
var rows []diffViewerRow
var dels, ins []int
flush := func() {
for j := 0; j < max(len(dels), len(ins)); j++ {
r := diffViewerRow{left: -1, right: -1}
if j < len(dels) {
r.left = dels[j]
}
if j < len(ins) {
r.right = ins[j]
}
rows = append(rows, r)
}
dels, ins = dels[:0], ins[:0]
}
line := func(i int) {
switch {
case !split:
rows = append(rows, diffViewerRow{left: i, right: -1})
case diff[i].Op == internal.DiffDelete:
dels = append(dels, i)
case diff[i].Op == internal.DiffInsert:
ins = append(ins, i)
default:
flush()
rows = append(rows, diffViewerRow{left: i, right: i})
}
}
for i := 0; i < len(diff); {
if diff[i].Op != internal.DiffEqual {
line(i)
i++
continue
}
end := i
for end < len(diff) && diff[end].Op == internal.DiffEqual {
end++
}
keepHead, keepTail := ctx, ctx
if i == 0 {
keepHead = 0
}
if end == len(diff) {
keepTail = 0
}
hidden := end - i - keepHead - keepTail
if hidden <= 1 || expanded[i+keepHead] {
for j := i; j < end; j++ {
line(j)
}
} else {
for j := i; j < i+keepHead; j++ {
line(j)
}
flush()
rows = append(rows, diffViewerRow{left: -1, right: -1, fold: i + keepHead, hidden: hidden})
for j := end - keepTail; j < end; j++ {
line(j)
}
}
i = end
}
flush()
return rows
}