Files
MujicaUI/git/pull_request_card.go

182 lines
6.1 KiB
Go

package git
import (
"fmt"
"log/slog"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/core"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/display"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/icons"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/internal"
"git.zacharyzhang.com/ZacharyZhang-NY/MujicaUI/theme"
)
// PullRequestState is where a pull request stands.
type PullRequestState int
const (
PullRequestOpen PullRequestState = iota
PullRequestDraft
PullRequestMerged
PullRequestClosed
)
// ReviewState is a reviewer's verdict.
type ReviewState int
const (
ReviewPending ReviewState = iota
ReviewApproved
ReviewChangesRequested
)
// Reviewer is a person asked to review.
type Reviewer struct {
Name string
State ReviewState
}
// PullRequest is what a PullRequestCard shows; the caller fetches it.
type PullRequest struct {
Number int
Title string
Author string
State PullRequestState
Head, Base string
// Checks counts CI results.
ChecksPassed, ChecksFailed, ChecksPending int
Reviewers []Reviewer
Added, Deleted, Files int
}
// PullRequestCardOptions configure a PullRequestCard.
type PullRequestCardOptions struct {
Disabled bool
}
// PullRequestCard summarizes a pull request; Changed reports opening.
func PullRequestCard(c *ui.Context, pr PullRequest, opts PullRequestCardOptions) core.ChoiceResult {
if pr.Number <= 0 || pr.Title == "" {
panic("mujica: PullRequest needs a positive Number and a Title")
}
if pr.ChecksPassed < 0 || pr.ChecksFailed < 0 || pr.ChecksPending < 0 || pr.Files < 0 {
panic("mujica: PullRequest counts must not be negative")
}
k := core.Tokens(c)
u := core.Density(c).Unit()
small := core.FontSize(c, theme.CaptionSize)
card := ui.Column(c).AlignItems(ui.Stretch).Gap(u*2).Padding(u*4).Background(k.Surface).
Border(theme.BorderWidth, k.Border).Radius(theme.CardRadius).Disabled(opts.Disabled)
res := core.ChoiceResult{Element: card}
card.Children(func() {
ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() {
pullRequestStateBadge(c, pr.State)
title := ui.ButtonBase(c).Justify(ui.Start).Gap(u*2).Padding(u, u).Radius(theme.ControlRadius).
Role(ui.RoleLink).Label(fmt.Sprintf("#%d %s", pr.Number, pr.Title)).Shrink(1).MinWidth(0).Cursor(ui.CursorPointer)
internal.Ring(title, k.Focus, theme.ControlRadius)
if title.Hovered() {
title.Background(k.SurfaceHover)
}
title.Transition(core.ColorFade)
title.Children(func() {
ui.Text(c, pr.Title).FontWeight(600).SingleLine().MinWidth(0)
ui.Text(c, fmt.Sprintf("#%d", pr.Number)).TextColor(k.TextMuted).FontFeatures("tnum").Shrink(0)
})
if title.Clicked() && !title.IsDisabled() {
res = core.NewChoiceResult(res.Element, true)
slog.Debug("mujica: pull request card open", "number", pr.Number)
}
})
ui.Row(c).AlignItems(ui.Center).Gap(u * 2).FontSize(small).TextColor(k.TextMuted).Wrap().Children(func() {
ui.Text(c, pr.Author).FontWeight(600).TextColor(k.Text)
if pr.Head != "" && pr.Base != "" {
ui.Icon(c, icons.Must("git-pull-request")).Size(12, 12)
ui.Text(c, pr.Head+" → "+pr.Base).Font(theme.MonoFont).SingleLine().MaxWidth(320)
}
})
ui.Row(c).AlignItems(ui.Center).Gap(u * 4).Wrap().FontSize(small).Children(func() {
pullRequestChecks(c, pr)
if len(pr.Reviewers) > 0 {
ui.Row(c).AlignItems(ui.Center).Gap(u * 2).Children(func() {
for _, r := range pr.Reviewers {
pullRequestReviewer(c, r)
}
})
}
ui.Spacer(c)
ui.Text(c, fmt.Sprintf(core.Msg(c, "pull_request_card.files"), pr.Files)).TextColor(k.TextMuted).FontFeatures("tnum")
DiffStat(c, pr.Added, pr.Deleted, DiffStatOptions{})
})
})
return res
}
func pullRequestStateBadge(c *ui.Context, st PullRequestState) {
k := core.Tokens(c)
u := core.Density(c).Unit()
icon, col, key := "git-pull-request", k.Success, "open"
switch st {
case PullRequestOpen:
case PullRequestDraft:
icon, col, key = "git-pull-request", k.TextMuted, "draft"
case PullRequestMerged:
icon, col, key = "git-merge", k.AccentText, "merged"
case PullRequestClosed:
icon, col, key = "circle-x", k.Danger, "closed"
default:
panic("mujica: unknown PullRequestState")
}
ui.Row(c).AlignItems(ui.Center).Gap(u).Padding(2, u*2).Radius(theme.ControlRadius).Border(theme.BorderWidth, col).
TextColor(col).Shrink(0).Children(func() {
ui.Icon(c, icons.Must(icon)).Size(14, 14)
ui.Text(c, core.Msg(c, "pull_request_card."+key)).FontSize(core.FontSize(c, theme.CaptionSize)).FontWeight(600).SingleLine()
})
}
func pullRequestChecks(c *ui.Context, pr PullRequest) {
k := core.Tokens(c)
u := core.Density(c).Unit()
ui.Row(c).AlignItems(ui.Center).Gap(u * 3).Children(func() {
for _, ch := range []struct {
n int
icon, key string
col ui.Color
}{
{pr.ChecksPassed, "circle-check", "pull_request_card.passed", k.Success},
{pr.ChecksFailed, "circle-x", "pull_request_card.failed", k.Danger},
{pr.ChecksPending, "clock", "pull_request_card.pending", k.Warning},
} {
if ch.n == 0 {
continue
}
name := fmt.Sprintf(core.Msg(c, ch.key), ch.n)
ui.Row(c).AlignItems(ui.Center).Gap(u).TextColor(ch.col).Label(name).Tooltip(name).Children(func() {
ui.Icon(c, icons.Must(ch.icon)).Size(14, 14)
ui.Text(c, fmt.Sprint(ch.n)).FontFeatures("tnum")
})
}
})
}
func pullRequestReviewer(c *ui.Context, r Reviewer) {
k := core.Tokens(c)
key, icon, col := "pull_request_card.review_pending", "clock", k.TextMuted
switch r.State {
case ReviewPending:
case ReviewApproved:
key, icon, col = "pull_request_card.approved", "check", k.Success
case ReviewChangesRequested:
key, icon, col = "pull_request_card.changes", "circle-alert", k.Danger
default:
panic("mujica: unknown ReviewState")
}
name := r.Name + ": " + core.Msg(c, key)
ui.Box(c).Size(28, 28).Shrink(0).Label(name).Tooltip(name).Children(func() {
display.Avatar(c, r.Name, display.AvatarOptions{Size: 24}).Absolute().Left(0).Top(0)
ui.Box(c).Size(14, 14).Radius(7).Background(k.Surface).Center().Absolute().Right(0).Bottom(0).TextColor(col).
Children(func() { ui.Icon(c, icons.Must(icon)).Size(10, 10) })
})
}