Add project components 421-435; Checkbox turns only when on

Fifteen project components: QuestBoard, TaskItem, TaskList, Milestones,
Burndown, Pomodoro, Priority, AssigneePicker, LabelPicker, TimeTracker,
Roadmap, SprintPanel, IssueCard, Checklist and WorkloadBar.

QuestBoard is one base that draws TODO, DOING and DONE; arrows move,
Alt with an arrow carries a card, and a drag carries it live. Priority
draws 12x12 bars, and Critical is on fire. Pomodoro banks time on PAUSE
and the tomato sleeps through the rest. Extreme inputs fail fast or
saturate instead of wrapping.

MyGo's CheckboxBase turns its value before Disabled is set, so
input.Checkbox now hands it a copy and keeps only turns of an enabled
base. The home scene test checks the sky row, not one pixel that
falling snow can cover.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-12 10:25:07 -04:00
1 parent 552b073e81
commit c8d058799d
30 files changed
+2496 -7

No files matched your search

+113
View File
@@ -0,0 +1,113 @@
package gallery
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/project"
)
var projectEntries = []Entry{
{421, "QuestBoard", "任务板", "project", "Quests in three columns. Drag a card across, or hold Alt with the arrows.", demoQuestBoard},
{422, "TaskItem", "任务项", "project", "A box to tick, a title, how urgent, and who.", demoTaskItem},
{423, "TaskList", "任务列表", "project", "Tasks with OPEN, DONE and URGENT chips to narrow them down.", demoTaskList},
{424, "Milestones", "里程碑", "project", "Flags along the road. The little hero stands at today.", demoMilestones},
{425, "Burndown", "燃尽图", "project", "The straight line is the plan. The other one is what happened.", demoBurndown},
{426, "Pomodoro", "番茄钟", "project", "Twenty-five minutes of work, five of rest. The tomato naps on the break.", demoPomodoro},
{427, "Priority", "优先级", "project", "LOW, MEDIUM, HIGH and, for the worst, CRITICAL: it is on fire.", demoPriority},
{428, "AssigneePicker", "指派", "project", "The party lines up. Pick who takes the quest, or nobody.", demoAssigneePicker},
}
type projectDemo struct {
board [3][]string
task project.Task
tasks []project.Task
assignee, changes int
}
func projectState(c *ui.Context) *projectDemo {
return ui.Local(c.Root(), demoKey("project"), func() projectDemo {
return projectDemo{
board: [3][]string{{"SLAY DRAGON", "FIND THE KEY", "SAVE VILLAGE"}, {"GRIND XP"}, {"TALK TO OLD MAN"}},
task: project.Task{Title: "Fix the save bug", Priority: project.High, Assignee: "Brom"},
tasks: []project.Task{
{Title: "Feed the slime", Priority: project.Low, Assignee: "Aria"},
{Title: "Fix the save bug", Priority: project.High, Assignee: "Brom", Done: true},
{Title: "Ship it", Priority: project.Critical},
{Title: "Draw the map", Priority: project.Medium, Assignee: "Cass"},
},
assignee: -1,
}
})
}
func demoQuestBoard(c *ui.Context, log func(string)) {
s := projectState(c)
if project.QuestBoard(c, "Quests", &s.board, project.QuestBoardOptions{}).Moved {
log(fmt.Sprintf("moved: %d to do, %d doing, %d done", len(s.board[0]), len(s.board[1]), len(s.board[2])))
}
}
func demoTaskItem(c *ui.Context, log func(string)) {
s := projectState(c)
if project.TaskItem(c, &s.task, project.TaskItemOptions{}).Changed {
log(fmt.Sprintf("done: %v", s.task.Done))
}
}
func demoTaskList(c *ui.Context, log func(string)) {
s := projectState(c)
if project.TaskList(c, "Tasks", s.tasks, project.TaskListOptions{}).Changed {
s.changes++
log(fmt.Sprintf("change %d", s.changes))
}
}
func demoMilestones(c *ui.Context, log func(string)) {
now := pixel.K(c).Now()
d := func(days int) time.Time { return now.AddDate(0, 0, days) }
project.Milestones(c, "Road", []project.Milestone{
{Title: "ALPHA", Date: d(-40), Done: true}, {Title: "BETA", Date: d(-12), Done: true},
{Title: "DEMO", Date: d(9)}, {Title: "LAUNCH", Date: d(30)}, {Title: "DLC", Date: d(60)},
}, project.MilestonesOptions{})
}
func demoBurndown(c *ui.Context, log func(string)) {
project.Burndown(c, "Sprint 7", 40, 10, []float64{40, 37, 35, 30, 28, 22}, project.BurndownOptions{})
}
func demoPomodoro(c *ui.Context, log func(string)) {
r := project.Pomodoro(c, "Tomato", project.PomodoroOptions{})
if r.Changed {
log("button")
}
if r.Rang {
log("ring")
}
}
func demoPriority(c *ui.Context, log func(string)) {
k := pixel.K(c)
ui.Column(c).Gap(k.D(4)).Children(func() {
for _, l := range []project.Level{project.Low, project.Medium, project.High, project.Critical} {
ui.Row(c).Gap(k.D(6)).AlignItems(ui.Center).Children(func() {
project.Priority(c, l, project.PriorityOptions{Scale: 2})
pixel.Text(c, k.Msg([]string{"low", "medium", "high", "critical"}[l]), pixel.TextOptions{})
})
}
})
}
func demoAssigneePicker(c *ui.Context, log func(string)) {
s := projectState(c)
if project.AssigneePicker(c, "Party", party[:4], &s.assignee, project.AssigneePickerOptions{}).Changed {
who := "nobody"
if s.assignee >= 0 {
who = party[s.assignee]
}
log("assigned to " + who)
}
}
+104
View File
@@ -0,0 +1,104 @@
package gallery
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/pixel"
"git.zacharyzhang.com/ZacharyZhang-NY/8bitMyGoUI/project"
)
var teamEntries = []Entry{
{429, "LabelPicker", "标签选择", "project", "Labels in slots. Toggle the ones that fit.", demoLabelPicker},
{430, "TimeTracker", "计时记录", "project", "START, STOP, and where the hours went.", demoTimeTracker},
{431, "Roadmap", "路线图", "project", "Lanes of work laid over the months. The dashed line is today.", demoRoadmap},
{432, "SprintPanel", "迭代", "project", "Points done and days left. No pressure.", demoSprintPanel},
{433, "IssueCard", "问题卡", "project", "An issue at a glance: number, title, labels, owner. Click to open.", demoIssueCard},
{434, "Checklist", "清单", "project", "Small steps under a bar. Finish them all for a fanfare.", demoChecklist},
{435, "WorkloadBar", "工作量", "project", "Hours per party member. Gold is busy; red is too much.", demoWorkloadBar},
}
var tags = []project.Label{
{Name: "BUG", Slot: pixel.Bad}, {Name: "FEATURE", Slot: pixel.Good}, {Name: "DOCS", Slot: pixel.Info},
{Name: "UI", Slot: pixel.Magic}, {Name: "SPEED", Slot: pixel.Warn}, {Name: "LORE", Slot: pixel.Accent},
}
type teamDemo struct {
on []bool
entries []project.TimeEntry
steps []bool
}
func teamState(c *ui.Context) *teamDemo {
now := pixel.K(c).Now()
return ui.Local(c.Root(), demoKey("team"), func() teamDemo {
return teamDemo{
on: []bool{true, false, false, true, false, false},
entries: []project.TimeEntry{
{Task: "GRIND XP", Start: now.Add(-3 * time.Hour), Stop: now.Add(-2*time.Hour - 35*time.Minute)},
{Task: "MAP THE CAVE", Start: now.Add(-90 * time.Minute), Stop: now.Add(-48 * time.Minute)},
},
steps: []bool{true, true, false, false},
}
})
}
func demoLabelPicker(c *ui.Context, log func(string)) {
s := teamState(c)
if project.LabelPicker(c, "Labels", tags, s.on, project.LabelPickerOptions{}).Changed {
n := 0
for _, on := range s.on {
if on {
n++
}
}
log(fmt.Sprintf("%d labels on", n))
}
}
func demoTimeTracker(c *ui.Context, log func(string)) {
s := teamState(c)
r := project.TimeTracker(c, "Time", "GRIND XP", &s.entries, project.TimeTrackerOptions{})
if r.Started {
log("started")
}
if r.Stopped {
log("stopped")
}
}
func demoRoadmap(c *ui.Context, log func(string)) {
now := pixel.K(c).Now()
from := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location()).AddDate(0, -1, 0)
m := func(month, day int) time.Time { return from.AddDate(0, month, day) }
project.Roadmap(c, "Plan", from, []project.Lane{
{Name: "ENGINE", Bars: []project.Bar{{Title: "PALETTES", Start: m(0, 0), End: m(1, 10)}, {Title: "SPRITES", Start: m(1, 14), End: m(3, 0)}}},
{Name: "GAME", Bars: []project.Bar{{Title: "LEVEL 1", Start: m(1, 0), End: m(4, 0)}}},
{Name: "SOUND", Bars: []project.Bar{{Title: "CHIPTUNE", Start: m(3, 10), End: m(5, 20)}}},
}, project.RoadmapOptions{})
}
func demoSprintPanel(c *ui.Context, log func(string)) {
now := pixel.K(c).Now()
project.SprintPanel(c, project.Sprint{Name: "SPRINT 7: THE DUNGEON", Start: now.AddDate(0, 0, -9), End: now.AddDate(0, 0, 4), Done: 18, Total: 30}, project.SprintPanelOptions{})
}
func demoIssueCard(c *ui.Context, log func(string)) {
is := project.Issue{ID: "#42", Title: "Save file vanishes after the boss", Labels: tags[:1], Assignee: "Brom", Priority: project.Critical}
if project.IssueCard(c, is, project.IssueCardOptions{}).Clicked {
log("open " + is.ID)
}
}
func demoChecklist(c *ui.Context, log func(string)) {
s := teamState(c)
if project.Checklist(c, "Before the quest", []string{"Sharpen the sword", "Pack potions", "Tell mom", "Leave the village"}, s.steps, project.ChecklistOptions{}).Changed {
log(fmt.Sprintf("steps %v", s.steps))
}
}
func demoWorkloadBar(c *ui.Context, log func(string)) {
project.WorkloadBar(c, "Load", []project.Load{{Name: "Aria", Hours: 32}, {Name: "Brom", Hours: 44}, {Name: "Cass", Hours: 18}, {Name: "Dax", Hours: 39}}, project.WorkloadBarOptions{})
}
+11 -4
View File
@@ -76,10 +76,17 @@ func TestHomeSceneFollowsThePaletteStrip(t *testing.T) {
if !ok {
t.Fatalf("the scene kept its season: %q", tt.Texts())
}
// The banner's sky starts in Winter Night's darkest slot.
got, want := tt.Image().RGBAAt(int(r.X)+1, int(r.Y)+1), pixel.WinterNight.Colors[pixel.Well]
if got.R != want.R || got.G != want.G || got.B != want.B {
t.Fatalf("the banner's sky is %v, want %v", got, want)
// The sky's top row is Winter Night's Well, snow aside.
want, well, n := pixel.WinterNight.Colors[pixel.Well], 0, 0
for x := int(r.X); x < int(r.X+r.W); x += 2 {
got := tt.Image().RGBAAt(x, int(r.Y)+1)
if got.R == want.R && got.G == want.G && got.B == want.B {
well++
}
n++
}
if well*4 < n*3 {
t.Fatalf("only %d of %d sky dots are Winter Night's Well", well, n)
}
if _, ok := tt.Find("Winter Night, in use"); !ok {
t.Fatal("the strip does not mark the palette in use")
+37
View File
@@ -0,0 +1,37 @@
package gallery
import (
"strings"
"testing"
"github.com/egoist/mygo/ui"
)
func TestProjectDemosLogTheirActions(t *testing.T) {
// Clicks that only focus a component may log nothing.
quiet := map[string]bool{"Quests": true}
for _, tc := range []struct {
path string
steps []string
}{
{"/c/421-questboard", []string{"Quests", "alt:Right"}}, {"/c/422-taskitem", []string{"Fix the save bug"}},
{"/c/423-tasklist", []string{"OPEN"}}, {"/c/426-pomodoro", []string{"START", "PAUSE"}},
{"/c/428-assigneepicker", []string{"Brom"}}, {"/c/429-labelpicker", []string{"Labels"}},
{"/c/430-timetracker", []string{"START", "STOP"}}, {"/c/433-issuecard", []string{"#42 Save file vanishes after the boss"}},
{"/c/434-checklist", []string{"Tell mom"}},
} {
a, tt := open(tc.path)
slug := tc.path[3:]
for _, step := range tc.steps {
before := len(a.logs[slug])
if k, ok := strings.CutPrefix(step, "alt:"); ok {
tt.Key(ui.Alt, letters[k])
} else {
click(t, tt, step)
}
if len(a.logs[slug]) <= before && !quiet[step] {
t.Errorf("%s: %s logged nothing: %q", slug, step, a.logs[slug])
}
}
}
}
+1 -1
View File
@@ -56,7 +56,7 @@ var Categories = []Category{
{"tabletop", "Tabletop", "桌游", icon.Crown},
}
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries, chatEntries, mailEntries, mediaEntries, studioEntries, codeEntries, debugEntries, accountEntries, playerEntries, filesEntries, storageEntries)
var entries = slices.Concat(foundationEntries, layoutEntries, textEntries, displayEntries, inputEntries, controlEntries, feedbackEntries, navigationEntries, dataEntries, recordEntries, overlayEntries, popupEntries, datetimeEntries, timeEntries, chartEntries, plotEntries, gameEntries, rpgEntries, screenEntries, lootEntries, fxEntries, screenFxEntries, chatEntries, mailEntries, mediaEntries, studioEntries, codeEntries, debugEntries, accountEntries, playerEntries, filesEntries, storageEntries, projectEntries, teamEntries)
func Entries() []Entry { return entries }