Merge branch 'main' into worktree-agent-ab9ba373bc47c2352

This commit is contained in:
ZacharyZhang-NY committed 2026-10-05 14:39:12 -04:00
commit eb78b0a4a1
1005 files changed
+62360 -18

No files matched your search

+280 -1
View File
@@ -1,3 +1,282 @@
package main
var agentPages = []page{}
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
const agentCategory = "Agent"
var agentPages = []page{
{ID: "261", Name: "ToolCallCard", ZhName: "工具调用卡片", Category: agentCategory, Demo: toolCallCardDemo, Code: toolCallCardCode,
Keys: "Tab 到参数、结果折叠按钮,Enter / Space 展开或折叠。"},
{ID: "262", Name: "ToolCallGroup", ZhName: "工具调用组", Category: agentCategory, Demo: toolCallGroupDemo, Code: toolCallGroupCode,
Keys: "Tab 到组标题,Enter / Space 展开或折叠。"},
{ID: "263", Name: "ToolApprovalDialog", ZhName: "工具审批对话框", Category: agentCategory, Demo: toolApprovalDemo, Code: toolApprovalDialogCode,
Keys: "打开时焦点在拒绝;Enter 只触发聚焦按钮,Tab 到允许才能批准;Escape 拒绝。"},
{ID: "264", Name: "PermissionPrompt", ZhName: "权限请求", Category: agentCategory, Demo: permissionDemo, Code: permissionPromptCode,
Keys: "打开时焦点在拒绝;Tab 到始终允许、允许一次;Escape 拒绝。"},
{ID: "265", Name: "AgentStepList", ZhName: "执行步骤列表", Category: agentCategory, Demo: agentStepListDemo, Code: agentStepListCode,
Keys: "↑ ↓ 在步骤与重试间移动;Enter 展开详情或重试。"},
{ID: "266", Name: "AgentPlan", ZhName: "执行计划", Category: agentCategory, Demo: agentPlanDemo, Code: agentPlanCode,
Keys: "Space 勾选;Tab 到跳过或恢复;在输入框中按 Enter 新增。"},
{ID: "267", Name: "AgentStatus", ZhName: "Agent 状态", Category: agentCategory, Demo: agentStatusDemo, Code: agentStatusCode,
Keys: "不获取焦点;屏幕阅读器读出状态文字。"},
{ID: "268", Name: "AgentProgress", ZhName: "Agent 进度", Category: agentCategory, Demo: agentProgressDemo, Code: agentProgressCode,
Keys: "不获取焦点;当前动作以状态读出。"},
{ID: "269", Name: "SubAgentTree", ZhName: "子 Agent 树", Category: agentCategory, Demo: subAgentTreeDemo, Code: subAgentTreeCode,
Keys: "↑ ↓ 移动;→ 展开,← 折叠;Enter / Space 切换。"},
{ID: "270", Name: "FileChangeCard", ZhName: "文件变更卡片", Category: agentCategory, Demo: fileChangeCardDemo, Code: fileChangeCardCode,
Keys: "Tab 到打开差异、拒绝、接受或撤销,Enter / Space 触发。"},
{ID: "271", Name: "MultiFileDiffReview", ZhName: "多文件差异审阅", Category: agentCategory, Demo: multiFileDiffDemo, Code: multiFileDiffReviewCode,
Keys: "文件列表中 ↑ ↓ 移动,Enter 选择;Tab 到接受、拒绝、全部接受。"},
{ID: "272", Name: "CommandExecutionCard", ZhName: "命令执行卡片", Category: agentCategory, Demo: commandExecutionDemo, Code: commandExecutionCardCode,
Keys: "Tab 到中止,Enter / Space 请求中止;输出区可滚动。"},
{ID: "273", Name: "ScreenshotStream", ZhName: "Agent 画面流", Category: agentCategory, Demo: screenshotStreamDemo, Code: screenshotStreamCode,
Keys: "画面区中 ← → 逐帧,Space 暂停或恢复。"},
{ID: "274", Name: "ArtifactPanel", ZhName: "产物面板", Category: agentCategory, Demo: artifactPanelDemo, Code: artifactPanelCode,
Keys: "切换组中 ← → 移动、Enter 选择;Tab 到复制、导出。"},
{ID: "275", Name: "ArtifactVersionSwitcher", ZhName: "产物版本切换", Category: agentCategory, Demo: artifactVersionDemo, Code: artifactVersionSwitcherCode,
Keys: "焦点在组件内时 ← → 切换版本;Tab 到比较按钮。"},
{ID: "276", Name: "SandboxStatus", ZhName: "沙箱状态", Category: agentCategory, Demo: sandboxStatusDemo, Code: sandboxStatusCode,
Keys: "不获取焦点;剩余时间每秒刷新。"},
{ID: "277", Name: "MCPServerList", ZhName: "MCP 服务器列表", Category: agentCategory, Demo: mcpServerListDemo, Code: mcpServerListCode,
Keys: "Tab 到各行的启动、停止、重新连接,Enter / Space 触发。"},
{ID: "278", Name: "MemoryPanel", ZhName: "记忆面板", Category: agentCategory, Demo: memoryPanelDemo, Code: memoryPanelCode,
Keys: "Tab 到搜索、编辑、删除;删除确认默认聚焦取消,Escape 取消。"},
{ID: "279", Name: "CheckpointList", ZhName: "检查点列表", Category: agentCategory, Demo: checkpointListDemo, Code: checkpointListCode,
Keys: "Tab 到回滚,Enter 打开确认;确认框默认聚焦取消。"},
{ID: "280", Name: "HumanInputRequest", ZhName: "人工输入请求", Category: agentCategory, Demo: humanInputDemo, Code: humanInputRequestCode,
Keys: "方向键选择选项;输入框中 Enter 发送;Tab 到发送回答。"},
}
// agentStates are the states a demo can pick from.
var agentStates = []string{"Pending", "Planning", "Running", "Waiting", "Done", "Failed", "Skipped"}
// agentStatePick is a select over agentStates, logging changes.
func agentStatePick(c *ui.Context, log func(string), v *string) mujica.AgentState {
if ui.Select(c, v, agentStates).Changed() {
log("state = " + *v)
}
for i, s := range agentStates {
if s == *v {
return mujica.AgentState(i)
}
}
panic("gallery: unknown state " + *v)
}
type toolCallDemoState struct {
state string
failed bool
still bool
}
func toolCallCardDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "261", func() toolCallDemoState { return toolCallDemoState{state: "Done"} })
var st mujica.AgentState
overlayControls(c, func() {
st = agentStatePick(c, log, &s.state)
overlayCheck(c, log, &s.failed, "Error")
overlayCheck(c, log, &s.still, "Still")
})
call := mujica.ToolCall{ID: "1", Name: "read_file", Args: `{"path":"court/ledger.md","limit":40}`,
Result: `{"lines":40,"truncated":false}`, State: st, Duration: 420 * time.Millisecond}
if s.failed {
call.Error = "permission denied: court/ledger.md"
}
if mujica.ToolCallCard(c, call, mujica.ToolCallCardOptions{Still: s.still}).Changed() {
log("folded or unfolded a section")
}
}
func toolCallGroupDemo(c *ui.Context, log func(string)) {
open := ui.Local(c.Root(), "262", func() bool { return true })
calls := []mujica.ToolCall{
{ID: "a", Name: "list_dir", Args: `{"path":"court"}`, Result: `["ledger.md","menu.md"]`, State: mujica.AgentDone, Duration: 300 * time.Millisecond},
{ID: "b", Name: "read_file", Args: `{"path":"court/wine.md"}`, Error: "not found", State: mujica.AgentFailed, Duration: 1200 * time.Millisecond},
{ID: "c", Name: "grep", Args: `{"pattern":"Sakiko"}`, Result: `{"matches":3}`, State: mujica.AgentDone, Duration: 500 * time.Millisecond},
}
if mujica.ToolCallGroup(c, open, calls, mujica.ToolCallGroupOptions{}).Changed() {
log(fmt.Sprintf("group open = %v", *open))
}
}
func toolApprovalDemo(c *ui.Context, log func(string)) {
open := ui.Local(c.Root(), "263", func() bool { return false })
if mujica.Button(c, "Run delete_files", mujica.ButtonOptions{}).Clicked() {
*open = true
log("asked for approval")
}
r := mujica.ToolApprovalDialog(c, open, mujica.ToolApprovalOptions{Tool: "delete_files",
Args: `{"glob":"build/*.tmp","dry_run":false}`, Reason: "The agent wants to clean temporary files from the build folder."})
if r.Allowed {
log("allowed")
}
if r.Denied {
log("denied")
}
}
type permissionDemoState struct {
open bool
grant string
}
func permissionDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "264", func() permissionDemoState { return permissionDemoState{grant: "none"} })
ui.Row(c).Gap(12).AlignItems(ui.Center).Children(func() {
if mujica.Button(c, "Ask to write", mujica.ButtonOptions{}).Clicked() {
s.open = true
log("asked for permission")
}
ui.Text(c, "Grant: "+s.grant)
})
r := mujica.PermissionPrompt(c, &s.open, mujica.PermissionPromptOptions{Title: "Write to the ledger?",
Description: "The agent wants to record tonight's guests.", Scope: "Write access to ~/court/ledger and its subfolders"})
if r.Choice != mujica.PermissionNone {
s.grant = [...]string{"none", "once", "always", "denied"}[r.Choice]
log("choice = " + s.grant)
}
}
type stepDemoState struct {
steps []mujica.AgentStep
disabled bool
}
func agentStepListDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "265", func() stepDemoState {
return stepDemoState{steps: []mujica.AgentStep{
{ID: "read", Title: "Read the guest list", State: mujica.AgentDone, Duration: 800 * time.Millisecond, Detail: "42 guests, 3 arrive late."},
{ID: "seat", Title: "Assign the seats", State: mujica.AgentFailed, Duration: 2 * time.Second, Detail: "Table 4 is over capacity."},
{ID: "send", Title: "Send the invitations", State: mujica.AgentPending},
}}
})
overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") })
r := mujica.AgentStepList(c, s.steps, mujica.AgentStepListOptions{Disabled: s.disabled})
if r.Toggled != "" {
log("toggled " + r.Toggled)
}
if r.Retry != "" {
log("retry " + r.Retry)
for i := range s.steps {
if s.steps[i].ID == r.Retry {
s.steps[i].State, s.steps[i].Detail = mujica.AgentRunning, "Retrying with a larger table."
}
}
}
}
type planDemoState struct {
items []mujica.PlanItem
disabled bool
}
func agentPlanDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "266", func() planDemoState {
return planDemoState{items: []mujica.PlanItem{
{ID: "a", Text: "Read the guest list", State: mujica.AgentDone},
{ID: "b", Text: "Assign the seats", State: mujica.AgentRunning},
{ID: "c", Text: "Order the flowers"},
}}
})
overlayControls(c, func() {
overlayCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Agent finishes a step", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
for i := range s.items {
if s.items[i].State != mujica.AgentDone && s.items[i].State != mujica.AgentSkipped {
s.items[i].State = mujica.AgentDone
log("agent checked " + s.items[i].Text)
break
}
}
}
})
if mujica.AgentPlan(c, &s.items, mujica.AgentPlanOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("plan changed (%d items)", len(s.items)))
}
}
type statusDemoState struct {
state string
still bool
}
func agentStatusDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "267", func() statusDemoState { return statusDemoState{state: "Running"} })
var st mujica.AgentState
overlayControls(c, func() {
st = agentStatePick(c, log, &s.state)
overlayCheck(c, log, &s.still, "Still")
})
ui.Row(c).Gap(8).Wrap().Children(func() {
mujica.AgentStatus(c, st, mujica.AgentStatusOptions{Still: s.still})
for _, v := range []mujica.AgentState{mujica.AgentPlanning, mujica.AgentRunning, mujica.AgentWaiting, mujica.AgentDone, mujica.AgentFailed} {
mujica.AgentStatus(c, v, mujica.AgentStatusOptions{Still: s.still})
}
})
}
type progressDemoState struct {
value float64
indeterminate bool
}
func agentProgressDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "268", func() progressDemoState { return progressDemoState{value: 0.4} })
overlayControls(c, func() {
if ui.Slider(c, &s.value, 0, 1).Width(200).Changed() {
log(fmt.Sprintf("value = %.2f", s.value))
}
overlayCheck(c, log, &s.indeterminate, "Indeterminate")
})
action := "Reading ledger.md"
if s.value > 0.5 {
action = "Writing the seating chart"
}
mujica.AgentProgress(c, s.value, mujica.AgentProgressOptions{Label: "Preparing the ball", Action: action, Indeterminate: s.indeterminate})
}
func subAgentTreeDemo(c *ui.Context, log func(string)) {
disabled := ui.Local(c.Root(), "269", func() bool { return false })
overlayControls(c, func() { overlayCheck(c, log, disabled, "Disabled") })
agents := []mujica.SubAgent{{ID: "lead", Name: "Steward", Task: "Prepare the ball", State: mujica.AgentRunning, Children: []mujica.SubAgent{
{ID: "music", Name: "Musician", Task: "Hire the quartet", State: mujica.AgentDone},
{ID: "kitchen", Name: "Cook", Task: "Plan the menu", State: mujica.AgentWaiting, Children: []mujica.SubAgent{
{ID: "wine", Name: "Sommelier", Task: "Choose the wine", State: mujica.AgentFailed},
}},
}}}
if mujica.SubAgentTree(c, agents, mujica.SubAgentTreeOptions{Disabled: *disabled}).Changed() {
log("expanded or collapsed an agent")
}
}
type fileChangeDemoState struct {
decision mujica.FileDecision
disabled bool
preview float64
}
func fileChangeCardDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "270", func() fileChangeDemoState { return fileChangeDemoState{preview: 8} })
overlayControls(c, func() {
overlayCheck(c, log, &s.disabled, "Disabled")
overlaySlider(c, log, &s.preview, "Preview", "lines", 2, 12, 1)
})
r := mujica.FileChangeCard(c, &s.decision, agentDemoChange, mujica.FileChangeCardOptions{Preview: int(s.preview), Disabled: s.disabled})
if r.Decided {
log(fmt.Sprintf("decision = %d", s.decision))
}
if r.OpenDiff {
log("open the full diff")
}
}
var agentDemoChange = mujica.FileChange{Path: "court/ledger.md",
Old: "# Ledger\nGuests: 40\nWine: claret\nMusic: none\nHall: east wing\n",
New: "# Ledger\nGuests: 42\nWine: claret\nMusic: quartet\nFlowers: lilies\nHall: east wing\n"}
+196
View File
@@ -0,0 +1,196 @@
package main
// Code snippets of the Agent pages, the same as their compiled Examples.
const toolCallCardCode = `call := mujica.ToolCall{ID: "1", Name: "read_file", Args: "{\"path\":\"ledger.md\"}",
Result: "{\"lines\":40}", State: mujica.AgentDone, Duration: 420 * time.Millisecond}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ToolCallCard(c, call, mujica.ToolCallCardOptions{OpenArgs: true})
}
ui.Render(view, 400, 300, 1)`
const toolCallGroupCode = `open := false
calls := []mujica.ToolCall{
{ID: "a", Name: "list_dir", State: mujica.AgentDone, Duration: 300 * time.Millisecond},
{ID: "b", Name: "read_file", State: mujica.AgentFailed, Error: "not found"},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ToolCallGroup(c, &open, calls, mujica.ToolCallGroupOptions{})
}
ui.Render(view, 400, 300, 1)`
const toolApprovalDialogCode = `open := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.ToolApprovalDialog(c, &open, mujica.ToolApprovalOptions{
Tool: "delete_files", Args: "{\"glob\":\"*.tmp\"}", Reason: "Clean the build folder.",
})
if r.Allowed {
fmt.Println("run the tool")
}
}
ui.Render(view, 400, 300, 1)`
const permissionPromptCode = `open := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.PermissionPrompt(c, &open, mujica.PermissionPromptOptions{
Title: "Write to the ledger?", Scope: "Write access to ~/court/ledger",
})
if r.Choice == mujica.PermissionAllowAlways {
fmt.Println("remember the grant")
}
}
ui.Render(view, 400, 300, 1)`
const agentStepListCode = `steps := []mujica.AgentStep{
{ID: "read", Title: "Read the guest list", State: mujica.AgentDone, Duration: time.Second},
{ID: "seat", Title: "Assign the seats", State: mujica.AgentFailed, Detail: "Table 4 is full."},
{ID: "send", Title: "Send invitations", State: mujica.AgentPending},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.AgentStepList(c, steps, mujica.AgentStepListOptions{}); r.Retry != "" {
fmt.Println("retry", r.Retry)
}
}
ui.Render(view, 400, 300, 1)`
const agentPlanCode = `items := []mujica.PlanItem{
{ID: "a", Text: "Read the guest list", State: mujica.AgentDone},
{ID: "b", Text: "Assign the seats", State: mujica.AgentRunning},
{ID: "c", Text: "Order the flowers"},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AgentPlan(c, &items, mujica.AgentPlanOptions{})
}
ui.Render(view, 400, 300, 1)`
const agentStatusCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AgentStatus(c, mujica.AgentWaiting, mujica.AgentStatusOptions{})
}
ui.Render(view, 400, 300, 1)`
const agentProgressCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AgentProgress(c, 0.4, mujica.AgentProgressOptions{Action: "Reading ledger.md"})
}
ui.Render(view, 400, 300, 1)`
const subAgentTreeCode = `agents := []mujica.SubAgent{{ID: "lead", Name: "Steward", State: mujica.AgentRunning, Children: []mujica.SubAgent{
{ID: "cook", Name: "Cook", Task: "Plan the menu", State: mujica.AgentWaiting},
}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SubAgentTree(c, agents, mujica.SubAgentTreeOptions{})
}
ui.Render(view, 400, 300, 1)`
const fileChangeCardCode = `decision := mujica.FilePending
change := mujica.FileChange{Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.FileChangeCard(c, &decision, change, mujica.FileChangeCardOptions{}).OpenDiff {
fmt.Println("open the full diff")
}
}
ui.Render(view, 400, 300, 1)`
const multiFileDiffReviewCode = `files := []mujica.FileChange{
{Path: "ledger.md", Old: "Guests: 40\n", New: "Guests: 42\n"},
{Path: "menu.md", Old: "soup\n", New: "soup\ntart\n"},
}
decisions := make([]mujica.FileDecision, len(files))
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.MultiFileDiffReview(c, &decisions, files, mujica.MultiFileDiffReviewOptions{})
}
ui.Render(view, 400, 300, 1)`
const commandExecutionCardCode = `run := mujica.CommandRun{Command: "go test ./...", Dir: "~/court", Running: true, Output: "\x1b[32mok\x1b[0m court\n"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.CommandExecutionCard(c, run, mujica.CommandExecutionCardOptions{}).Changed() {
fmt.Println("abort the process")
}
}
ui.Render(view, 400, 300, 1)`
const screenshotStreamCode = `var st mujica.ScreenshotStreamState
var frames []mujica.ScreenshotFrame // appended as the agent captures them
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ScreenshotStream(c, &st, frames, mujica.ScreenshotStreamOptions{})
}
ui.Render(view, 400, 300, 1)`
const artifactPanelCode = `art := mujica.Artifact{Title: "seating.go", Language: "go", Content: "package court\n"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ArtifactPanel(c, art, mujica.ArtifactPanelOptions{}).Exported {
fmt.Println("save the file")
}
}
ui.Render(view, 400, 300, 1)`
const artifactVersionSwitcherCode = `current := 1
versions := []mujica.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added music"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ArtifactVersionSwitcher(c, &current, versions, mujica.ArtifactVersionSwitcherOptions{}).Compare {
fmt.Println("diff v1 and v2")
}
}
ui.Render(view, 400, 300, 1)`
const sandboxStatusCode = `info := mujica.SandboxInfo{Name: "sandbox-7", State: mujica.SandboxRunning,
Resources: []mujica.SandboxResource{{Name: "CPU", Used: 1.5, Limit: 2, Unit: "cores"}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SandboxStatus(c, info, mujica.SandboxStatusOptions{})
}
ui.Render(view, 400, 300, 1)`
const mcpServerListCode = `servers := []mujica.MCPServer{{ID: "fs", Name: "filesystem", Transport: "stdio", State: mujica.MCPConnected, Tools: 12}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.MCPServerList(c, servers, mujica.MCPServerListOptions{}); r.Action == mujica.MCPStop {
fmt.Println("stop", r.Server)
}
}
ui.Render(view, 400, 300, 1)`
const memoryPanelCode = `entries := []mujica.MemoryEntry{{ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.MemoryPanel(c, &entries, mujica.MemoryPanelOptions{}); r.Deleted != "" {
fmt.Println("forget", r.Deleted)
}
}
ui.Render(view, 400, 300, 1)`
const checkpointListCode = `current := "c2"
points := []mujica.Checkpoint{{ID: "c2", Label: "Menu drafted"}, {ID: "c1", Label: "Start"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.CheckpointList(c, &current, points, mujica.CheckpointListOptions{}).Changed() {
fmt.Println("restore", current)
}
}
ui.Render(view, 400, 300, 1)`
const humanInputRequestCode = `answer := ""
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.HumanInputRequest(c, &answer, mujica.HumanInputRequestOptions{
Question: "Which hall?", Options: []string{"East wing", "Great hall"}, AllowFree: true,
})
if r.Submitted {
fmt.Println("reply", answer)
}
}
ui.Render(view, 400, 300, 1)`
+285
View File
@@ -0,0 +1,285 @@
package main
import (
"fmt"
"image"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
type multiFileDemoState struct {
decisions []mujica.FileDecision
disabled bool
}
var agentDemoFiles = []mujica.FileChange{
agentDemoChange,
{Path: "court/menu.md", Old: "soup\nroast\n", New: "soup\nroast\ntart\n"},
{Path: "court/old-plan.md", Old: "Hold the ball in the cellar.\n"},
}
func multiFileDiffDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "271", func() multiFileDemoState {
return multiFileDemoState{decisions: make([]mujica.FileDecision, len(agentDemoFiles))}
})
overlayControls(c, func() {
overlayCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Reset", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
clear(s.decisions)
log("reset")
}
})
if mujica.MultiFileDiffReview(c, &s.decisions, agentDemoFiles, mujica.MultiFileDiffReviewOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("decisions = %v", s.decisions))
}
}
type commandDemoState struct {
run mujica.CommandRun
disabled bool
lines int
}
func commandExecutionDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "272", func() commandDemoState {
return commandDemoState{run: mujica.CommandRun{Command: "go test ./court/...", Dir: "~/projects/court", Running: true}}
})
overlayControls(c, func() {
overlayCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Print a line", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.lines++
color := []string{"32mok ", "33mwarn", "31mFAIL"}[s.lines%3]
s.run.Output += fmt.Sprintf("\x1b[%s\x1b[0m court/case%d 0.%02ds\n", color, s.lines, s.lines)
log("printed a line")
}
if mujica.Button(c, "Restart", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.run.Running, s.run.Output, s.lines = true, "", 0
log("restarted")
}
})
if mujica.CommandExecutionCard(c, s.run, mujica.CommandExecutionCardOptions{Disabled: s.disabled}).Changed() {
s.run.Running, s.run.ExitCode, s.run.Duration = false, 130, time.Duration(s.lines)*time.Second
s.run.Output += "\x1b[31maborted by user\x1b[0m\n"
log("abort requested")
}
}
type screenshotDemoState struct {
st mujica.ScreenshotStreamState
frames []mujica.ScreenshotFrame
disabled bool
}
// agentDemoFrame paints a frame with a moving block.
func agentDemoFrame(i int) mujica.ScreenshotFrame {
img := image.NewRGBA(image.Rect(0, 0, 320, 180))
for y := range 180 {
for x := range 320 {
o := img.PixOffset(x, y)
v := uint8(235)
if x >= 20+i*24%260 && x < 60+i*24%260 && y > 60 && y < 120 {
v = 120
}
img.Pix[o], img.Pix[o+1], img.Pix[o+2], img.Pix[o+3] = v, v-10, v-20, 255
}
}
return mujica.ScreenshotFrame{Image: ui.NewBitmap(img), Caption: fmt.Sprintf("Clicked button %d", i+1), At: time.Duration(i) * 2 * time.Second}
}
func screenshotStreamDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "273", func() screenshotDemoState {
return screenshotDemoState{frames: []mujica.ScreenshotFrame{agentDemoFrame(0), agentDemoFrame(1)}}
})
overlayControls(c, func() {
overlayCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Capture a frame", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.frames = append(s.frames, agentDemoFrame(len(s.frames)))
log(fmt.Sprintf("frame %d arrived", len(s.frames)))
}
})
if mujica.ScreenshotStream(c, &s.st, s.frames, mujica.ScreenshotStreamOptions{Height: 220, Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("frame %d, paused = %v", s.st.Index+1, s.st.Paused))
}
}
type artifactDemoState struct {
kind string
disabled bool
}
func artifactPanelDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "274", func() artifactDemoState { return artifactDemoState{kind: "Code"} })
overlayControls(c, func() {
if ui.Select(c, &s.kind, []string{"Code", "Document"}).Changed() {
log("kind = " + s.kind)
}
overlayCheck(c, log, &s.disabled, "Disabled")
})
art := mujica.Artifact{Title: "seating.go", Language: "go",
Content: "package court\n\n// Seats maps guests to tables.\nfunc Seats() map[string]int {\n\treturn map[string]int{\"Sakiko\": 1, \"Uika\": 2}\n}\n"}
opts := mujica.ArtifactPanelOptions{Height: 200, Disabled: s.disabled}
if s.kind == "Document" {
art = mujica.Artifact{Title: "Invitation", Kind: mujica.ArtifactDocument, Content: "# You are invited\n\nThe masquerade begins at nine.\n"}
opts.Preview = func() {
mujica.Heading(c, "You are invited", mujica.HeadingOptions{})
ui.Text(c, "The masquerade begins at nine.")
}
}
r := mujica.ArtifactPanel(c, art, opts)
if r.Copied {
log("copied to the clipboard")
}
if r.Exported {
log("export requested")
}
}
type versionDemoState struct {
current int
disabled bool
}
func artifactVersionDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "275", func() versionDemoState { return versionDemoState{current: 2} })
overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") })
versions := []mujica.ArtifactVersion{{Label: "v1", Note: "First draft"}, {Label: "v2", Note: "Added the quartet"}, {Label: "v3", Note: "Moved the wine table"}}
r := mujica.ArtifactVersionSwitcher(c, &s.current, versions, mujica.ArtifactVersionSwitcherOptions{Disabled: s.disabled})
if r.Changed {
log("version = " + versions[s.current].Label)
}
if r.Compare {
log("compare " + versions[s.current-1].Label + " and " + versions[s.current].Label)
}
}
type sandboxDemoState struct {
state string
cpu float64
expires time.Time
}
func sandboxStatusDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "276", func() sandboxDemoState {
return sandboxDemoState{state: "Running", cpu: 1.2, expires: time.Now().Add(10 * time.Minute)}
})
names := []string{"Starting", "Running", "Stopped", "Failed"}
state := mujica.SandboxStarting
overlayControls(c, func() {
if ui.Select(c, &s.state, names).Changed() {
log("state = " + s.state)
}
overlaySlider(c, log, &s.cpu, "CPU", "cores", 0, 2, 0.1)
if mujica.Button(c, "Extend 5 min", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.expires = s.expires.Add(5 * time.Minute)
log("extended")
}
})
for i, n := range names {
if n == s.state {
state = mujica.SandboxState(i)
}
}
mujica.SandboxStatus(c, mujica.SandboxInfo{Name: "sandbox-7", State: state, Expires: s.expires, Resources: []mujica.SandboxResource{
{Name: "CPU", Used: s.cpu, Limit: 2, Unit: "cores"}, {Name: "Memory", Used: 3.1, Limit: 4, Unit: "GB"},
}}, mujica.SandboxStatusOptions{})
}
type mcpDemoState struct {
servers []mujica.MCPServer
disabled bool
}
func mcpServerListDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "277", func() mcpDemoState {
return mcpDemoState{servers: []mujica.MCPServer{
{ID: "fs", Name: "filesystem", Transport: "stdio", State: mujica.MCPConnected, Tools: 12},
{ID: "gh", Name: "github", Transport: "http", State: mujica.MCPError, Error: "401 Unauthorized"},
{ID: "db", Name: "postgres", Transport: "stdio", State: mujica.MCPStopped},
}}
})
overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") })
r := mujica.MCPServerList(c, s.servers, mujica.MCPServerListOptions{Disabled: s.disabled})
if r.Action == mujica.MCPNone {
return
}
log(fmt.Sprintf("%s: %s", r.Server, [...]string{"", "start", "stop", "reconnect"}[r.Action]))
for i := range s.servers {
if s.servers[i].ID != r.Server {
continue
}
switch r.Action {
case mujica.MCPStop:
s.servers[i].State = mujica.MCPStopped
default:
s.servers[i].State, s.servers[i].Tools, s.servers[i].Error = mujica.MCPConnected, 5, ""
}
}
}
type memoryDemoState struct {
entries []mujica.MemoryEntry
disabled bool
}
func memoryPanelDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "278", func() memoryDemoState {
return memoryDemoState{entries: []mujica.MemoryEntry{
{ID: "1", Text: "Lady Sakiko prefers the east wing.", Source: "Chat, 3 Oct"},
{ID: "2", Text: "The quartet needs two days' notice.", Source: "Plan, 1 Oct"},
{ID: "3", Text: "Claret is served after nine.", Source: "Ledger"},
}}
})
overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") })
r := mujica.MemoryPanel(c, &s.entries, mujica.MemoryPanelOptions{Disabled: s.disabled})
if r.Edited != "" {
log("edited " + r.Edited)
}
if r.Deleted != "" {
log("deleted " + r.Deleted)
}
}
type checkpointDemoState struct {
current string
disabled bool
}
func checkpointListDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "279", func() checkpointDemoState { return checkpointDemoState{current: "c3"} })
overlayControls(c, func() { overlayCheck(c, log, &s.disabled, "Disabled") })
points := []mujica.Checkpoint{{ID: "c3", Label: "Seats assigned", Note: "12 files changed"},
{ID: "c2", Label: "Menu drafted", Note: "3 files changed"}, {ID: "c1", Label: "Start"}}
if mujica.CheckpointList(c, &s.current, points, mujica.CheckpointListOptions{Disabled: s.disabled}).Changed() {
log("rolled back to " + s.current)
}
}
type humanDemoState struct {
answer string
free bool
deadline time.Time
disabled bool
}
func humanInputDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "280", func() humanDemoState { return humanDemoState{free: true} })
overlayControls(c, func() {
overlayCheck(c, log, &s.free, "AllowFree")
overlayCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Start a 30 s timer", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.deadline = time.Now().Add(30 * time.Second)
log("timer started")
}
})
r := mujica.HumanInputRequest(c, &s.answer, mujica.HumanInputRequestOptions{Question: "Which hall should host the masquerade?",
Options: []string{"East wing", "Great hall"}, AllowFree: s.free, Deadline: s.deadline, Disabled: s.disabled})
if r.Submitted {
log("answer sent: " + s.answer)
}
if r.TimedOut {
log("timed out")
}
}
+44 -1
View File
@@ -1,3 +1,46 @@
package main
var calendarPages = []page{}
const calCategory = "Calendar"
var calendarPages = []page{
{ID: "421", Name: "CalendarMonthView", ZhName: "月视图", Category: calCategory, Demo: calMonthDemo, Code: calCalendarMonthViewCode,
Keys: "Tab reaches day numbers and event chips; arrows move between them; Enter opens; Alt+Left/Right moves an event a day, Alt+Up/Down a week; Escape closes the +N list."},
{ID: "422", Name: "CalendarWeekView", ZhName: "周视图", Category: calCategory, Demo: calWeekDemo, Code: calCalendarWeekViewCode,
Keys: "Tab reaches a day column; Up/Down move the slot cursor, Enter creates a one-slot event; on a chip Alt+Up/Down moves by a slot, Alt+Left/Right by a day, Alt+Shift+Up/Down resizes."},
{ID: "423", Name: "CalendarDayView", ZhName: "日视图", Category: calCategory, Demo: calDayDemo, Code: calCalendarDayViewCode,
Keys: "As the week view: Up/Down and Enter in the column, Alt with arrows on a chip."},
{ID: "424", Name: "AgendaView", ZhName: "日程列表", Category: calCategory, Demo: calAgendaDemo, Code: calAgendaViewCode,
Keys: "Tab enters the list; Up/Down move between events; Enter opens."},
{ID: "425", Name: "YearView", ZhName: "年视图", Category: calCategory, Demo: calYearDemo, Code: calYearViewCode,
Keys: "Tab enters the days; arrows move by day and week, PageUp/PageDown by month, across years; Enter chooses."},
{ID: "426", Name: "EventChip", ZhName: "事件块", Category: calCategory, Demo: calChipDemo, Code: calEventChipCode,
Keys: "Tab focuses the chip; Enter or Space activates it."},
{ID: "427", Name: "EventEditor", ZhName: "事件编辑器", Category: calCategory, Demo: calEditorDemo, Code: calEventEditorCode,
Keys: "Tab moves through the fields; each field keeps its own keys; Save and Cancel are buttons."},
{ID: "428", Name: "EventPopover", ZhName: "事件浮层", Category: calCategory, Demo: calPopoverDemo, Code: calEventPopoverCode,
Keys: "Enter on the chip opens it; Tab reaches Close, Delete and Edit; Escape closes."},
{ID: "429", Name: "CurrentTimeIndicator", ZhName: "当前时间线", Category: calCategory, Demo: calNowDemo, Code: calCurrentTimeIndicatorCode,
Keys: "Not focusable; screen readers read the time from its label."},
{ID: "430", Name: "RecurrenceEditor", ZhName: "重复规则编辑器", Category: calCategory, Demo: calRecurrenceDemo, Code: calRecurrenceEditorCode,
Keys: "Tab moves between the repeat select, interval, weekdays and end choices; each control keeps its own keys."},
{ID: "431", Name: "AvailabilityPicker", ZhName: "空闲时段选择", Category: calCategory, Demo: calAvailabilityDemo, Code: calAvailabilityPickerCode,
Keys: "Tab focuses the grid; arrows move the cursor; Space or Enter toggles the slot."},
{ID: "432", Name: "TimezoneSelect", ZhName: "时区选择", Category: calCategory, Demo: calTimezoneDemo, Code: calTimezoneSelectCode,
Keys: "Enter, Space or Down opens; type to search; Up/Down move; Enter chooses; Escape closes."},
{ID: "433", Name: "ReminderPicker", ZhName: "提醒选择", Category: calCategory, Demo: calReminderDemo, Code: calReminderPickerCode,
Keys: "Tab enters the list; Up/Down move; Space toggles."},
{ID: "434", Name: "AttendeeList", ZhName: "参与者列表", Category: calCategory, Demo: calAttendeeDemo, Code: calAttendeeListCode,
Keys: "Tab reaches each remove button; Enter removes."},
{ID: "435", Name: "WeekPicker", ZhName: "周选择器", Category: calCategory, Demo: calWeekPickerDemo, Code: calWeekPickerCode,
Keys: "Enter, Space or Down opens; arrows move; Enter picks the week; Escape closes."},
{ID: "436", Name: "DurationPicker", ZhName: "时长选择器", Category: calCategory, Demo: calDurationDemo, Code: calDurationPickerCode,
Keys: "Tab moves between hours, minutes and seconds; Up/Down step; type digits and Enter."},
{ID: "437", Name: "CronEditor", ZhName: "Cron 编辑器", Category: calCategory, Demo: calCronDemo, Code: calCronEditorCode,
Keys: "Type in the expression field; errors and the next runs update as you type."},
{ID: "438", Name: "RelativeTime", ZhName: "相对时间", Category: calCategory, Demo: calRelativeDemo, Code: calRelativeTimeCode,
Keys: "Not focusable; the exact time is in its label and tooltip."},
{ID: "439", Name: "Countdown", ZhName: "倒计时", Category: calCategory, Demo: calCountdownDemo, Code: calCountdownCode,
Keys: "Not focusable; reaching zero is announced."},
{ID: "440", Name: "Stopwatch", ZhName: "秒表", Category: calCategory, Demo: calStopwatchDemo, Code: calStopwatchCode,
Keys: "Tab reaches Start/Pause, Lap and Reset; Enter or Space presses."},
}
+253
View File
@@ -0,0 +1,253 @@
package main
// Code snippets mirror example_calendar_test.go.
const (
calCalendarMonthViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent {
start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc)
return []mujica.CalendarEvent{
{ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"},
{ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo},
}
}
loc := time.Local
events := exampleEvents(loc)
month := mujica.YearMonth{Year: 2026, Month: time.October}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.CalendarMonthView(c, &month, events, mujica.CalendarMonthViewOptions{Location: loc})
if ch, ok := r.Changed(); ok {
fmt.Println("move", ch.ID, "to", ch.Start)
}
}
ui.Render(view, 400, 300, 1)`
calCalendarWeekViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent {
start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc)
return []mujica.CalendarEvent{
{ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"},
{ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo},
}
}
loc := time.Local
events := exampleEvents(loc)
week := mujica.Date{Year: 2026, Month: time.October, Day: 14}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.CalendarWeekView(c, &week, events, mujica.CalendarWeekViewOptions{Location: loc, SlotMinutes: 15})
if ch, ok := r.Changed(); ok && ch.Kind == mujica.CalendarCreate {
fmt.Println("new event", ch.Start, ch.End)
}
}
ui.Render(view, 400, 300, 1)`
calCalendarDayViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent {
start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc)
return []mujica.CalendarEvent{
{ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"},
{ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo},
}
}
loc := time.Local
events := exampleEvents(loc)
day := mujica.Date{Year: 2026, Month: time.October, Day: 14}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if id, ok := mujica.CalendarDayView(c, &day, events, mujica.CalendarDayViewOptions{Location: loc}).Opened(); ok {
fmt.Println("open", id)
}
}
ui.Render(view, 400, 300, 1)`
calAgendaViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent {
start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc)
return []mujica.CalendarEvent{
{ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"},
{ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo},
}
}
loc := time.Local
events := exampleEvents(loc)
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
opts := mujica.AgendaViewOptions{Location: loc, From: mujica.Date{Year: 2026, Month: time.October, Day: 12}, Days: 14}
if id, ok := mujica.AgendaView(c, events, opts).Opened(); ok {
fmt.Println("open", id)
}
}
ui.Render(view, 400, 300, 1)`
calYearViewCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent {
start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc)
return []mujica.CalendarEvent{
{ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"},
{ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo},
}
}
loc := time.Local
events := exampleEvents(loc)
year := 2026
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if day, ok := mujica.YearView(c, &year, events, mujica.YearViewOptions{Location: loc}).DayChosen(); ok {
fmt.Println("go to", day)
}
}
ui.Render(view, 400, 300, 1)`
calEventChipCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent {
start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc)
return []mujica.CalendarEvent{
{ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"},
{ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo},
}
}
loc := time.Local
ev := exampleEvents(loc)[0]
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.EventChip(c, ev, mujica.EventChipOptions{Location: loc}).Clicked() {
fmt.Println("open", ev.ID)
}
}
ui.Render(view, 400, 300, 1)`
calEventEditorCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent {
start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc)
return []mujica.CalendarEvent{
{ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"},
{ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo},
}
}
loc := time.Local
ev := exampleEvents(loc)[0]
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.EventEditor(c, &ev, mujica.EventEditorOptions{Location: loc})
if r.Saved() {
fmt.Println("saved", ev.Title, ev.Start)
}
}
ui.Render(view, 400, 300, 1)`
calEventPopoverCode = `func exampleEvents(loc *time.Location) []mujica.CalendarEvent {
start := time.Date(2026, time.October, 14, 9, 0, 0, 0, loc)
return []mujica.CalendarEvent{
{ID: "rehearsal", Title: "Rehearsal", Start: start, End: start.Add(2 * time.Hour), Location: "Hall"},
{ID: "fitting", Title: "Fitting", Start: start.Add(time.Hour), End: start.Add(90 * time.Minute), Color: mujica.EventInfo},
}
}
loc := time.Local
ev := exampleEvents(loc)[0]
open := false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
chip := mujica.EventChip(c, ev, mujica.EventChipOptions{Location: loc})
if chip.Clicked() {
open = true
}
r := mujica.EventPopover(c, chip, &open, ev, mujica.EventPopoverOptions{Location: loc})
if r.Delete() {
fmt.Println("delete", ev.ID)
}
}
ui.Render(view, 400, 300, 1)`
calCurrentTimeIndicatorCode = `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
ui.Box(c).Size(200, 240).Children(func() {
mujica.CurrentTimeIndicator(c, mujica.CurrentTimeIndicatorOptions{Location: time.Local})
})
}
ui.Render(view, 400, 300, 1)`
calRecurrenceEditorCode = `rule := mujica.Recurrence{Unit: mujica.RepeatWeekly, Interval: 1, Weekdays: []time.Weekday{time.Monday}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.RecurrenceEditor(c, &rule, mujica.RecurrenceEditorOptions{Start: mujica.Date{Year: 2026, Month: time.October, Day: 5}})
if r.Changed() && r.Problem() == "" {
fmt.Println(mujica.RecurrenceSummary(c, rule))
}
}
ui.Render(view, 400, 300, 1)`
calAvailabilityPickerCode = `days := []mujica.Date{{Year: 2026, Month: time.October, Day: 12}, {Year: 2026, Month: time.October, Day: 13}}
mine := [][]bool{make([]bool, 18), make([]bool, 18)}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.AvailabilityPicker(c, &mine, mujica.AvailabilityPickerOptions{Days: days}).Changed() {
fmt.Println("availability changed")
}
}
ui.Render(view, 400, 300, 1)`
calTimezoneSelectCode = `zone := "Asia/Shanghai"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.TimezoneSelect(c, &zone, mujica.TimezoneSelectOptions{}).Changed() {
fmt.Println("zone", zone)
}
}
ui.Render(view, 400, 300, 1)`
calReminderPickerCode = `reminders := []time.Duration{15 * time.Minute}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ReminderPicker(c, &reminders, mujica.ReminderPickerOptions{Max: 3}).Changed() {
fmt.Println("reminders", reminders)
}
}
ui.Render(view, 400, 300, 1)`
calAttendeeListCode = `people := []mujica.Attendee{{Name: "Sakiko", Status: mujica.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if i, ok := mujica.AttendeeList(c, people, mujica.AttendeeListOptions{Removable: true}).Removed(); ok {
people = append(people[:i], people[i+1:]...)
}
}
ui.Render(view, 400, 300, 1)`
calWeekPickerCode = `var week mujica.Date
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.WeekPicker(c, &week, mujica.WeekPickerOptions{WeekStart: mujica.WeekStartMonday, Clearable: true}).Changed() {
fmt.Println("week of", week)
}
}
ui.Render(view, 400, 300, 1)`
calDurationPickerCode = `length := 90 * time.Minute
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
opts := mujica.DurationPickerOptions{Min: 15 * time.Minute, Max: 8 * time.Hour}
if mujica.DurationPicker(c, &length, opts).Changed() {
fmt.Println("length", length)
}
}
ui.Render(view, 400, 300, 1)`
calCronEditorCode = `expr := "0 9 * * MON-FRI"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.CronEditor(c, &expr, mujica.CronEditorOptions{Location: time.Local}).Changed() {
if _, err := mujica.ParseCron(expr); err == nil {
fmt.Println("schedule", expr)
}
}
}
ui.Render(view, 400, 300, 1)`
calRelativeTimeCode = `posted := time.Date(2026, time.October, 5, 9, 0, 0, 0, time.Local)
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RelativeTime(c, posted, mujica.RelativeTimeOptions{Location: time.Local})
}
ui.Render(view, 400, 300, 1)`
calCountdownCode = `deadline := time.Date(2026, time.December, 31, 23, 59, 59, 0, time.Local)
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Countdown(c, deadline, mujica.CountdownOptions{}).Finished() {
fmt.Println("happy new year")
}
}
ui.Render(view, 400, 300, 1)`
calStopwatchCode = `var sw mujica.StopwatchState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Stopwatch(c, &sw, mujica.StopwatchOptions{}).Changed() {
fmt.Println("laps", len(sw.Laps))
}
}
ui.Render(view, 400, 300, 1)`
)
+217
View File
@@ -0,0 +1,217 @@
package main
import (
"fmt"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
var calColors = []string{"Accent", "Info", "Success", "Warning", "Danger", "Neutral"}
func calChipDemo(c *ui.Context, log func(string)) {
type state struct {
color string
inline, selected, disabled bool
}
s := ui.Local(c.Root(), "426", func() state { return state{color: "Accent"} })
dtSection(c, "EventChip", func() {
dtControls(c, func() {
mujica.Select(c, &s.color, calOptions(calColors), mujica.SelectOptions{Label: "Color"})
mujica.Checkbox(c, &s.inline, "Inline", mujica.CheckboxOptions{})
mujica.Checkbox(c, &s.selected, "Selected", mujica.CheckboxOptions{})
mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{})
})
ev := calSample()[0]
for i, n := range calColors {
if n == s.color {
ev.Color = mujica.EventColor(i)
}
}
ui.Box(c).Width(240).Children(func() {
if mujica.EventChip(c, ev, mujica.EventChipOptions{Location: time.Local, Inline: s.inline, Selected: s.selected, Disabled: s.disabled}).Clicked() {
log("clicked " + ev.Title)
}
})
})
}
func calEditorDemo(c *ui.Context, log func(string)) {
type state struct {
ev mujica.CalendarEvent
disabled bool
}
s := ui.Local(c.Root(), "427", func() state { return state{ev: calSample()[0]} })
dtSection(c, "EventEditor", func() {
dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) })
r := mujica.EventEditor(c, &s.ev, mujica.EventEditorOptions{Location: time.Local, Disabled: s.disabled})
if r.Saved() {
log(fmt.Sprintf("saved %q %s, %d guests", s.ev.Title, s.ev.Start.Format("Jan 2 15:04"), len(s.ev.Attendees)))
}
if r.Canceled() {
log("canceled")
}
})
}
func calPopoverDemo(c *ui.Context, log func(string)) {
type state struct {
open, readOnly, disabled bool
}
s := ui.Local(c.Root(), "428", func() state { return state{} })
dtSection(c, "EventPopover", func() {
dtControls(c, func() {
mujica.Checkbox(c, &s.readOnly, "Read-only", mujica.CheckboxOptions{})
mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{})
})
ev := calSample()[0]
ev.Attendees = []mujica.Attendee{{Name: "Sakiko", Status: mujica.AttendeeAccepted, Organizer: true}, {Name: "Mutsumi"}}
ev.Reminders = []time.Duration{15 * time.Minute}
ui.Box(c).Width(240).Children(func() {
chip := mujica.EventChip(c, ev, mujica.EventChipOptions{Location: time.Local, Disabled: s.disabled})
if chip.Clicked() {
s.open = true
log("opened popover")
}
r := mujica.EventPopover(c, chip, &s.open, ev, mujica.EventPopoverOptions{Location: time.Local, ReadOnly: s.readOnly, Disabled: s.disabled})
if r.Edit() {
log("edit " + ev.ID)
}
if r.Delete() {
log("delete " + ev.ID)
}
})
})
}
func calNowDemo(c *ui.Context, log func(string)) {
dtSection(c, "CurrentTimeIndicator", func() {
k := mujica.Tokens(c)
ui.Box(c).Size(320, 240).Border(1, k.Border).Children(func() {
mujica.CurrentTimeIndicator(c, mujica.CurrentTimeIndicatorOptions{Location: time.Local})
})
})
}
func calRecurrenceDemo(c *ui.Context, log func(string)) {
type state struct {
rule mujica.Recurrence
disabled bool
}
s := ui.Local(c.Root(), "430", func() state { return state{} })
dtSection(c, "RecurrenceEditor", func() {
dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) })
r := mujica.RecurrenceEditor(c, &s.rule, mujica.RecurrenceEditorOptions{Start: mujica.DateOf(time.Now()), Disabled: s.disabled})
if r.Changed() {
log(mujica.RecurrenceSummary(c, s.rule) + strings.TrimSpace(" "+r.Problem()))
}
})
}
func calAvailabilityDemo(c *ui.Context, log func(string)) {
type state struct {
mine, theirs [][]bool
days []mujica.Date
others, frozen bool
}
s := ui.Local(c.Root(), "431", func() state {
today := mujica.DateOf(time.Now())
grid := func(seed int) [][]bool {
g := make([][]bool, 5)
for i := range g {
g[i] = make([]bool, 18)
for j := range g[i] {
g[i][j] = (i*7+j*3+seed)%4 == 0
}
}
return g
}
days := []mujica.Date{today, today.AddDays(1), today.AddDays(2), today.AddDays(3), today.AddDays(4)}
return state{mine: grid(2), theirs: grid(1), days: days, others: true}
})
dtSection(c, "AvailabilityPicker", func() {
dtControls(c, func() {
mujica.Checkbox(c, &s.others, "Show Mutsumi", mujica.CheckboxOptions{})
mujica.Checkbox(c, &s.frozen, "Disabled", mujica.CheckboxOptions{})
})
var others []mujica.AvailabilityPerson
if s.others {
others = []mujica.AvailabilityPerson{{Name: "Mutsumi", Free: s.theirs}}
}
if mujica.AvailabilityPicker(c, &s.mine, mujica.AvailabilityPickerOptions{Days: s.days, Others: others, Disabled: s.frozen}).Changed() {
n := 0
for _, d := range s.mine {
for _, v := range d {
if v {
n++
}
}
}
log(fmt.Sprintf("%d free slots", n))
}
})
}
func calTimezoneDemo(c *ui.Context, log func(string)) {
type state struct {
zone string
disabled bool
}
s := ui.Local(c.Root(), "432", func() state { return state{zone: "Asia/Shanghai"} })
dtSection(c, "TimezoneSelect", func() {
dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) })
if mujica.TimezoneSelect(c, &s.zone, mujica.TimezoneSelectOptions{Disabled: s.disabled}).Changed() {
log("zone " + s.zone)
}
})
}
func calReminderDemo(c *ui.Context, log func(string)) {
type state struct {
v []time.Duration
max string
disabled bool
}
s := ui.Local(c.Root(), "433", func() state { return state{v: []time.Duration{15 * time.Minute}, max: "0"} })
dtSection(c, "ReminderPicker", func() {
dtControls(c, func() {
mujica.Select(c, &s.max, calOptions([]string{"0", "2", "3"}), mujica.SelectOptions{Label: "Max (0 = any)"})
mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{})
})
if limit := calAtoi(s.max); limit > 0 && len(s.v) > limit {
s.v = s.v[:limit]
}
if mujica.ReminderPicker(c, &s.v, mujica.ReminderPickerOptions{Max: calAtoi(s.max), Disabled: s.disabled}).Changed() {
log(fmt.Sprint("reminders ", s.v))
}
})
}
func calAttendeeDemo(c *ui.Context, log func(string)) {
type state struct {
list []mujica.Attendee
removable bool
}
people := func() []mujica.Attendee {
return []mujica.Attendee{{Name: "Sakiko Togawa", Status: mujica.AttendeeAccepted, Organizer: true},
{Name: "Mutsumi Wakaba", Status: mujica.AttendeeTentative}, {Name: "Uika Misumi", Status: mujica.AttendeeDeclined},
{Name: "Nyamu Yuutenji", Email: "nyamu@example.com"}}
}
s := ui.Local(c.Root(), "434", func() state { return state{list: people(), removable: true} })
dtSection(c, "AttendeeList", func() {
dtControls(c, func() {
mujica.Checkbox(c, &s.removable, "Removable", mujica.CheckboxOptions{})
if mujica.Button(c, "Reset", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.list = people()
log("reset")
}
})
if i, ok := mujica.AttendeeList(c, s.list, mujica.AttendeeListOptions{Removable: s.removable}).Removed(); ok {
log("removed " + s.list[i].Name)
s.list = append(s.list[:i:i], s.list[i+1:]...)
}
})
}
+128
View File
@@ -0,0 +1,128 @@
package main
import (
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func calWeekPickerDemo(c *ui.Context, log func(string)) {
type state struct {
week mujica.Date
weekStart string
disabled bool
}
s := ui.Local(c.Root(), "435", func() state { return state{weekStart: "Locale"} })
dtSection(c, "WeekPicker", func() {
dtControls(c, func() {
if mujica.Select(c, &s.weekStart, calOptions(calWeekStarts), mujica.SelectOptions{Label: "Week start"}).Changed() {
s.week = mujica.Date{}
log("week start " + s.weekStart + ", value cleared")
}
mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{})
})
opts := mujica.WeekPickerOptions{WeekStart: calWeekStart(s.weekStart), Clearable: true, Disabled: s.disabled}
if mujica.WeekPicker(c, &s.week, opts).Changed() {
log("week of " + s.week.String())
}
})
}
func calDurationDemo(c *ui.Context, log func(string)) {
type state struct {
v time.Duration
seconds, disabled bool
}
s := ui.Local(c.Root(), "436", func() state { return state{v: 90 * time.Minute} })
dtSection(c, "DurationPicker", func() {
dtControls(c, func() {
mujica.Checkbox(c, &s.seconds, "Seconds", mujica.CheckboxOptions{})
mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{})
})
opts := mujica.DurationPickerOptions{Seconds: s.seconds, Min: 15 * time.Minute, Max: 8 * time.Hour, Disabled: s.disabled}
if mujica.DurationPicker(c, &s.v, opts).Changed() {
log("duration " + s.v.String())
}
})
}
func calCronDemo(c *ui.Context, log func(string)) {
type state struct {
expr string
disabled bool
}
s := ui.Local(c.Root(), "437", func() state { return state{expr: "*/15 9-17 * * MON-FRI"} })
dtSection(c, "CronEditor", func() {
dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) })
if mujica.CronEditor(c, &s.expr, mujica.CronEditorOptions{Location: time.Local, Disabled: s.disabled}).Changed() {
if _, err := mujica.ParseCron(s.expr); err != nil {
log("invalid: " + s.expr)
} else {
log("schedule " + s.expr)
}
}
})
}
func calRelativeDemo(c *ui.Context, log func(string)) {
type state struct{ at time.Time }
s := ui.Local(c.Root(), "438", func() state { return state{at: time.Now()} })
dtSection(c, "RelativeTime", func() {
dtControls(c, func() {
for _, b := range []struct {
label string
d time.Duration
}{{"Now", 0}, {"5 minutes ago", -5 * time.Minute}, {"In 3 hours", 3 * time.Hour}, {"40 days ago", -40 * 24 * time.Hour}} {
if mujica.Button(c, b.label, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.at = time.Now().Add(b.d)
log("time set: " + b.label)
}
}
})
mujica.RelativeTime(c, s.at, mujica.RelativeTimeOptions{Location: time.Local})
})
}
func calCountdownDemo(c *ui.Context, log func(string)) {
type state struct {
deadline time.Time
words bool
}
s := ui.Local(c.Root(), "439", func() state { return state{deadline: time.Now().Add(10 * time.Second)} })
dtSection(c, "Countdown", func() {
dtControls(c, func() {
if mujica.Button(c, "Restart 10 s", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.deadline = time.Now().Add(10 * time.Second)
log("restarted")
}
mujica.Checkbox(c, &s.words, "Words", mujica.CheckboxOptions{})
})
format := mujica.CountdownClock
if s.words {
format = mujica.CountdownWords
}
if mujica.Countdown(c, s.deadline, mujica.CountdownOptions{Format: format}).Finished() {
log("finished")
}
})
}
func calStopwatchDemo(c *ui.Context, log func(string)) {
type state struct {
sw mujica.StopwatchState
disabled bool
}
s := ui.Local(c.Root(), "440", func() state { return state{} })
dtSection(c, "Stopwatch", func() {
dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) })
if mujica.Stopwatch(c, &s.sw, mujica.StopwatchOptions{Disabled: s.disabled}).Changed() {
state := "paused"
if s.sw.Running() {
state = "running"
}
log(state + ", " + mujica.StopwatchText(s.sw.Elapsed(time.Now())))
}
})
}
+194
View File
@@ -0,0 +1,194 @@
package main
import (
"fmt"
"strconv"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
// calSample is demo events around today in the local zone.
func calSample() []mujica.CalendarEvent {
now := time.Now()
day := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local)
at := func(d, h, m, mins int) (time.Time, time.Time) {
s := day.AddDate(0, 0, d).Add(time.Duration(h)*time.Hour + time.Duration(m)*time.Minute)
return s, s.Add(time.Duration(mins) * time.Minute)
}
mk := func(id, title string, color mujica.EventColor, d, h, m, mins int) mujica.CalendarEvent {
s, e := at(d, h, m, mins)
return mujica.CalendarEvent{ID: id, Title: title, Color: color, Start: s, End: e, Location: "Rehearsal hall"}
}
evs := []mujica.CalendarEvent{
mk("1", "Rehearsal", mujica.EventAccent, 0, 9, 0, 120), mk("2", "Costume fitting", mujica.EventInfo, 0, 10, 0, 60),
mk("3", "Lunch", mujica.EventSuccess, 0, 12, 30, 60), mk("4", "Lyrics review", mujica.EventWarning, 1, 14, 0, 90),
mk("5", "Press", mujica.EventDanger, 2, 16, 0, 45), mk("6", "Sound check", mujica.EventNeutral, 3, 18, 0, 60),
mk("7", "Encore notes", mujica.EventInfo, 0, 15, 0, 30), mk("8", "Stage walk", mujica.EventAccent, 0, 17, 0, 30),
}
holiday := mk("9", "Masquerade", mujica.EventAccent, 4, 0, 0, 24*60)
holiday.AllDay, holiday.Start = true, day.AddDate(0, 0, 4)
holiday.End = holiday.Start.AddDate(0, 0, 1)
return append(evs, holiday)
}
// calApply applies and logs a view change.
func calApply(evs *[]mujica.CalendarEvent, ch mujica.CalendarChange, log func(string)) {
switch ch.Kind {
case mujica.CalendarCreate:
id := fmt.Sprint(len(*evs) + 100)
*evs = append(*evs, mujica.CalendarEvent{ID: id, Title: "New event", Start: ch.Start, End: ch.End})
log("created " + ch.Start.Format("Jan 2 15:04") + " – " + ch.End.Format("15:04"))
return
}
for i := range *evs {
if (*evs)[i].ID == ch.ID {
(*evs)[i].Start, (*evs)[i].End = ch.Start, ch.End
}
}
verb := "moved"
if ch.Kind == mujica.CalendarResize {
verb = "resized"
}
log(fmt.Sprintf("%s %s to %s – %s", verb, ch.ID, ch.Start.Format("Jan 2 15:04"), ch.End.Format("15:04")))
}
// calViewResult logs what a view reported and applies its change.
func calViewResult(r mujica.CalendarViewResult, evs *[]mujica.CalendarEvent, version *int, log func(string)) {
if id, ok := r.Opened(); ok {
log("opened " + id)
}
if d, ok := r.DayChosen(); ok {
log("day " + d.String())
}
if ch, ok := r.Changed(); ok {
calApply(evs, ch, log)
*version++
}
}
var calWeekStarts = []string{"Locale", "Sunday", "Monday", "Saturday"}
func calWeekStart(s string) mujica.WeekStart {
for i, n := range calWeekStarts {
if n == s {
return mujica.WeekStart(i)
}
}
panic("gallery: unknown week start " + s)
}
func calMonthDemo(c *ui.Context, log func(string)) {
type state struct {
month mujica.YearMonth
events []mujica.CalendarEvent
version, maxRows int
weekStart, maxStr string
disabled bool
}
s := ui.Local(c.Root(), "421", func() state {
now := time.Now()
return state{month: mujica.YearMonth{Year: now.Year(), Month: now.Month()}, events: calSample(), weekStart: "Locale", maxStr: "3"}
})
dtSection(c, "CalendarMonthView", func() {
dtControls(c, func() {
mujica.Select(c, &s.weekStart, calOptions(calWeekStarts), mujica.SelectOptions{Label: "Week start"})
mujica.Select(c, &s.maxStr, calOptions([]string{"1", "2", "3", "4"}), mujica.SelectOptions{Label: "Rows per day"})
mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{})
})
s.maxRows = calAtoi(s.maxStr)
r := mujica.CalendarMonthView(c, &s.month, s.events, mujica.CalendarMonthViewOptions{Location: time.Local, Version: s.version,
WeekStart: calWeekStart(s.weekStart), MaxPerDay: s.maxRows, Disabled: s.disabled})
calViewResult(r, &s.events, &s.version, log)
})
}
func calWeekDemo(c *ui.Context, log func(string)) {
type state struct {
week mujica.Date
events []mujica.CalendarEvent
version int
slot string
hour12 bool
disabled bool
}
s := ui.Local(c.Root(), "422", func() state {
return state{week: mujica.DateOf(time.Now()), events: calSample(), slot: "30"}
})
dtSection(c, "CalendarWeekView", func() {
dtControls(c, func() {
mujica.Select(c, &s.slot, calOptions([]string{"15", "30", "60"}), mujica.SelectOptions{Label: "Slot minutes"})
mujica.Checkbox(c, &s.hour12, "12-hour", mujica.CheckboxOptions{})
mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{})
})
slot := calAtoi(s.slot)
r := mujica.CalendarWeekView(c, &s.week, s.events, mujica.CalendarWeekViewOptions{Location: time.Local, Version: s.version,
SlotMinutes: slot, Hour12: s.hour12, Disabled: s.disabled})
calViewResult(r, &s.events, &s.version, log)
})
}
func calDayDemo(c *ui.Context, log func(string)) {
type state struct {
day mujica.Date
events []mujica.CalendarEvent
version int
disabled bool
}
s := ui.Local(c.Root(), "423", func() state { return state{day: mujica.DateOf(time.Now()), events: calSample()} })
dtSection(c, "CalendarDayView", func() {
dtControls(c, func() { mujica.Checkbox(c, &s.disabled, "Disabled", mujica.CheckboxOptions{}) })
r := mujica.CalendarDayView(c, &s.day, s.events, mujica.CalendarDayViewOptions{Location: time.Local, Version: s.version, Disabled: s.disabled})
calViewResult(r, &s.events, &s.version, log)
})
}
func calAgendaDemo(c *ui.Context, log func(string)) {
type state struct {
events []mujica.CalendarEvent
days string
}
s := ui.Local(c.Root(), "424", func() state { return state{events: calSample(), days: "7"} })
dtSection(c, "AgendaView", func() {
dtControls(c, func() {
mujica.Select(c, &s.days, calOptions([]string{"1", "7", "30"}), mujica.SelectOptions{Label: "Days"})
})
days := calAtoi(s.days)
ui.Column(c).Height(420).Children(func() {
if id, ok := mujica.AgendaView(c, s.events, mujica.AgendaViewOptions{Location: time.Local, Days: days}).Opened(); ok {
log("opened " + id)
}
})
})
}
func calYearDemo(c *ui.Context, log func(string)) {
type state struct {
year int
events []mujica.CalendarEvent
}
s := ui.Local(c.Root(), "425", func() state { return state{year: time.Now().Year(), events: calSample()} })
dtSection(c, "YearView", func() {
if d, ok := mujica.YearView(c, &s.year, s.events, mujica.YearViewOptions{Location: time.Local}).DayChosen(); ok {
log("day " + d.String())
}
})
}
func calAtoi(s string) int {
n, err := strconv.Atoi(s)
if err != nil {
panic(err)
}
return n
}
func calOptions(values []string) []mujica.SelectOption[string] {
out := make([]mujica.SelectOption[string], len(values))
for i, v := range values {
out[i] = mujica.SelectOption[string]{Value: v, Label: v}
}
return out
}
+75 -1
View File
@@ -1,3 +1,77 @@
package main
var charts1Pages = []page{}
import (
"fmt"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/theme"
)
const c1Category = "Charts I"
const c1Keys = "Tab focuses the chart (legend check boxes first); arrows move between points and the readout is announced; Home/End jump; Escape clears."
var charts1Pages = []page{
{ID: "101", Name: "LineChart", ZhName: "折线图", Category: c1Category, Demo: c1LineDemo, Code: c1LineCode, Keys: c1Keys + " Space on a legend entry hides or shows its series."},
{ID: "102", Name: "AreaChart", ZhName: "面积图", Category: c1Category, Demo: c1AreaDemo, Code: c1AreaCode, Keys: c1Keys},
{ID: "103", Name: "BarChart", ZhName: "柱状图", Category: c1Category, Demo: c1BarDemo, Code: c1BarCode, Keys: c1Keys + " Arrows step bar by bar."},
{ID: "104", Name: "PieChart", ZhName: "饼图", Category: c1Category, Demo: c1PieDemo, Code: c1PieCode, Keys: c1Keys},
{ID: "105", Name: "ScatterChart", ZhName: "散点图", Category: c1Category, Demo: c1ScatterDemo, Code: c1ScatterCode, Keys: c1Keys + " Enter or Space selects the point; drag a box to select."},
{ID: "106", Name: "RadarChart", ZhName: "雷达图", Category: c1Category, Demo: c1RadarDemo, Code: c1RadarCode, Keys: c1Keys},
{ID: "107", Name: "HeatmapChart", ZhName: "热力图", Category: c1Category, Demo: c1HeatDemo, Code: c1HeatCode, Keys: "Tab focuses the map; Left/Right move along a row, Up/Down along a column; Home/End jump; Escape clears."},
{ID: "108", Name: "CalendarHeatmap", ZhName: "日历热力图", Category: c1Category, Demo: c1CalendarDemo, Code: c1CalendarCode, Keys: "Left/Right move a week, Up/Down a day; Home/End jump to the year's ends; the year buttons take Enter or Space."},
{ID: "109", Name: "Histogram", ZhName: "直方图", Category: c1Category, Demo: c1HistogramDemo, Code: c1HistogramCode, Keys: c1Keys + " Arrows step bin by bin."},
{ID: "110", Name: "BoxPlot", ZhName: "箱线图", Category: c1Category, Demo: c1BoxDemo, Code: c1BoxCode, Keys: c1Keys + " Arrows step group by group."},
{ID: "111", Name: "WaterfallChart", ZhName: "瀑布图", Category: c1Category, Demo: c1WaterfallDemo, Code: c1WaterfallCode, Keys: c1Keys},
{ID: "112", Name: "FunnelChart", ZhName: "漏斗图", Category: c1Category, Demo: c1FunnelDemo, Code: c1FunnelCode, Keys: c1Keys + " Arrows step stage by stage."},
{ID: "113", Name: "BulletChart", ZhName: "子弹图", Category: c1Category, Demo: c1BulletDemo, Code: c1BulletCode, Keys: c1Keys + " Arrows read the value, the target, then each range."},
{ID: "114", Name: "Sparkline", ZhName: "迷你图", Category: c1Category, Demo: c1SparkDemo, Code: c1SparkCode, Keys: "Tab focuses the sparkline; Left/Right read points; Home/End jump; Escape shows the last value again."},
{ID: "115", Name: "Gauge", ZhName: "仪表盘", Category: c1Category, Demo: c1GaugeDemo, Code: c1GaugeCode, Keys: c1Keys + " Arrows read the value, then each segment."},
{ID: "116", Name: "Treemap", ZhName: "矩形树图", Category: c1Category, Demo: c1TreemapDemo, Code: c1TreemapCode, Keys: c1Keys + " Enter or Space drills into a branch; Backspace goes back; Back and the breadcrumb take Tab."},
{ID: "117", Name: "Sunburst", ZhName: "旭日图", Category: c1Category, Demo: c1SunburstDemo, Code: c1SunburstCode, Keys: c1Keys + " Enter or Space drills in; Backspace goes back."},
{ID: "118", Name: "SankeyChart", ZhName: "桑基图", Category: c1Category, Demo: c1SankeyDemo, Code: c1SankeyCode, Keys: c1Keys + " Reading a node or link highlights every path through it."},
{ID: "119", Name: "NetworkGraph", ZhName: "网络图", Category: c1Category, Demo: c1NetworkDemo, Code: c1NetworkCode, Keys: c1Keys + " Shift+arrows move the node; +/- zoom; 0 resets the view; drag nodes or the background."},
{ID: "120", Name: "RealtimeChart", ZhName: "实时图表", Category: c1Category, Demo: c1RealtimeDemo, Code: c1RealtimeCode, Keys: "Tab reaches Pause, the legend and the chart; chart keys as in LineChart."},
}
var c1Statuses = []string{"Ready", "Loading", "Failed", "Empty"}
// c1Frame shows the status picker and returns the frame.
func c1Frame(c *ui.Context, label string, status *string) (mujica.ChartFrame, bool) {
ui.Select(c, status, c1Statuses).Width(160).AlignSelf(ui.Start)
f := mujica.ChartFrame{Label: label}
switch *status {
case "Loading":
f.Status = mujica.DataLoading
case "Failed":
f.Status, f.Error = mujica.DataFailed, "The archive did not answer"
}
return f, *status != "Empty"
}
// c1Section lays out a title, controls, then the chart.
func c1Section(c *ui.Context, title string, controls, body func()) {
ui.Column(c).Gap(12).Children(func() {
ui.Text(c, title).Font(theme.SerifFont).FontSize(mujica.FontSize(c, theme.H3Size))
ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Children(controls)
ui.Column(c).Width(560).Children(body)
})
}
// c1Log records a chart's events in the gallery log.
func c1Log(log func(string), name string, r mujica.ChartResult) {
if r.Changed() {
log(fmt.Sprintf("%s: point %d", name, r.Point))
}
if s, ok := r.Toggled(); ok {
log(name + ": toggled " + s)
}
if r.ValueChanged() {
log(name + ": value changed")
}
if r.Retried() {
log(name + ": retry")
}
}
+174
View File
@@ -0,0 +1,174 @@
package main
// Snippets are the bodies of the Examples in example_charts1_test.go.
const c1LineCode = `series := []mujica.ChartSeries{
{Name: "Pews", Values: []float64{3, 5, math.NaN(), 4, 8}},
{Name: "Candles", Values: []float64{5, 2, 6, 7, 1}},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.LineChart(c, series, mujica.LineChartOptions{Frame: mujica.ChartFrame{Label: "Chapel"}})
}
ui.Render(view, 400, 300, 1)`
const c1AreaCode = `series := []mujica.ChartSeries{
{Name: "Matins", Values: []float64{3, 4, 6, 5}},
{Name: "Vespers", Values: []float64{2, 3, 2, 4}},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AreaChart(c, series, mujica.AreaChartOptions{Frame: mujica.ChartFrame{Label: "Attendance"}, Stacked: true})
}
ui.Render(view, 400, 300, 1)`
const c1BarCode = `categories := []string{"Nave", "Choir", "Crypt"}
series := []mujica.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3}}, {Name: "Silver", Values: []float64{2, 5, 1}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.BarChart(c, categories, series, mujica.BarChartOptions{Frame: mujica.ChartFrame{Label: "Tithes"}, Labels: true})
}
ui.Render(view, 400, 300, 1)`
const c1PieCode = `slices := []mujica.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 3}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PieChart(c, slices, mujica.PieChartOptions{Frame: mujica.ChartFrame{Label: "Stores"}, Donut: true, MinAngle: 20})
}
ui.Render(view, 400, 300, 1)`
const c1ScatterCode = `var selected []mujica.ScatterKey
groups := []mujica.ScatterGroup{{Name: "Monks", Points: []mujica.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ScatterChart(c, &selected, groups, mujica.ScatterChartOptions{Frame: mujica.ChartFrame{Label: "Choir"}, Bubbles: true})
}
ui.Render(view, 400, 300, 1)`
const c1RadarCode = `axes := []string{"Voice", "Latin", "Chant", "Organ"}
series := []mujica.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RadarChart(c, axes, series, mujica.RadarChartOptions{Frame: mujica.ChartFrame{Label: "Skills"}})
}
ui.Render(view, 400, 300, 1)`
const c1HeatCode = `rows, cols := []string{"Mon", "Tue"}, []string{"Matins", "Lauds", "Vespers"}
values := [][]float64{{1, 4, 9}, {3, math.NaN(), 7}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.HeatmapChart(c, rows, cols, values, mujica.HeatmapChartOptions{Frame: mujica.ChartFrame{Label: "Hours"}})
}
ui.Render(view, 400, 300, 1)`
const c1CalendarCode = `year := 2026
values := map[mujica.Date]float64{{Year: 2026, Month: time.March, Day: 4}: 5}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.CalendarHeatmap(c, &year, values, mujica.CalendarHeatmapOptions{Frame: mujica.ChartFrame{Label: "Prayers"}})
}
ui.Render(view, 400, 300, 1)`
const c1HistogramCode = `values := []float64{1, 2, 2, 3, 4, 5, 9, 10}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Histogram(c, values, mujica.HistogramOptions{Frame: mujica.ChartFrame{Label: "Ages"}, BinWidth: 5})
}
ui.Render(view, 400, 300, 1)`
const c1BoxCode = `groups := []mujica.BoxGroup{{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 40}}, {Name: "Nave", Values: []float64{4, 5, 6, 7}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.BoxPlot(c, groups, mujica.BoxPlotOptions{Frame: mujica.ChartFrame{Label: "Hymns"}})
}
ui.Render(view, 400, 300, 1)`
const c1WaterfallCode = `steps := []mujica.WaterfallStep{
{Label: "Tithes", Value: 12},
{Label: "Repairs", Value: -5},
{Label: "Year", Kind: mujica.WaterfallTotal},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.WaterfallChart(c, steps, mujica.WaterfallChartOptions{Frame: mujica.ChartFrame{Label: "Ledger"}})
}
ui.Render(view, 400, 300, 1)`
const c1FunnelCode = `stages := []mujica.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.FunnelChart(c, stages, mujica.FunnelChartOptions{Frame: mujica.ChartFrame{Label: "Order"}})
}
ui.Render(view, 400, 300, 1)`
const c1BulletCode = `target := 250.0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.BulletChart(c, 270, mujica.BulletChartOptions{Frame: mujica.ChartFrame{Label: "Alms"}, Target: &target, Bands: []float64{150, 225, 300}})
}
ui.Render(view, 400, 300, 1)`
const c1SparkCode = `values := []float64{3, 5, 4, 7, 6, 9}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Sparkline(c, values, mujica.SparklineOptions{Frame: mujica.ChartFrame{Label: "Bells"}, Kind: mujica.SparklineArea})
}
ui.Render(view, 400, 300, 1)`
const c1GaugeCode = `segments := []mujica.GaugeSegment{{To: 60, Label: "Calm", Tone: mujica.GaugeGood}, {To: 100, Label: "Crowded", Tone: mujica.GaugeDanger}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Gauge(c, 72, mujica.GaugeOptions{Frame: mujica.ChartFrame{Label: "Nave"}, Max: 100, Unit: "%", Segments: segments})
}
ui.Render(view, 400, 300, 1)`
const c1TreemapCode = `var path []string
root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{
{Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}},
{Name: "Kitchen", Value: 25},
}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Treemap(c, &path, root, mujica.TreemapOptions{Frame: mujica.ChartFrame{Label: "Rooms"}})
}
ui.Render(view, 400, 300, 1)`
const c1SunburstCode = `var path []string
root := mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{
{Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}}},
{Name: "Kitchen", Value: 25},
}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Sunburst(c, &path, root, mujica.SunburstOptions{Frame: mujica.ChartFrame{Label: "Rooms"}})
}
ui.Render(view, 400, 300, 1)`
const c1SankeyCode = `nodes := []mujica.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}}
links := []mujica.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "fund", To: "roof", Value: 50}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SankeyChart(c, nodes, links, mujica.SankeyChartOptions{Frame: mujica.ChartFrame{Label: "Money"}})
}
ui.Render(view, 400, 300, 1)`
const c1NetworkCode = `var state mujica.NetworkGraphState
nodes := []mujica.GraphNode{{ID: "a", Label: "Abbot"}, {ID: "b", Label: "Prior"}, {ID: "c", Label: "Novice"}}
edges := []mujica.GraphEdge{{From: "a", To: "b"}, {From: "b", To: "c"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.NetworkGraph(c, &state, nodes, edges, mujica.NetworkGraphOptions{Frame: mujica.ChartFrame{Label: "House"}, Force: true})
}
ui.Render(view, 400, 300, 1)`
const c1RealtimeCode = `buf := mujica.NewRealtimeBuffer(600, "Bell")
start := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
for i := range 60 {
buf.Append(start.Add(time.Duration(i)*time.Second), float64(i%7))
}
var state mujica.RealtimeChartState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RealtimeChart(c, &state, buf, mujica.RealtimeChartOptions{Frame: mujica.ChartFrame{Label: "Sound"}, Window: 30 * time.Second})
}
ui.Render(view, 400, 300, 1)`
+137
View File
@@ -0,0 +1,137 @@
package main
import (
"fmt"
"math"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
var c1Abbey = mujica.ChartNode{Name: "Abbey", Children: []mujica.ChartNode{
{Name: "Library", Children: []mujica.ChartNode{{Name: "Psalters", Value: 30}, {Name: "Maps", Value: 10}, {Name: "Letters", Value: 8}}},
{Name: "Kitchen", Value: 25},
{Name: "Garden", Children: []mujica.ChartNode{{Name: "Herbs", Value: 12}, {Name: "Vines", Value: 8}}},
{Name: "Chapel", Children: []mujica.ChartNode{{Name: "Nave", Value: 18}, {Name: "Crypt", Value: 6}}},
}}
func c1TreemapDemo(c *ui.Context, log func(string)) {
type state struct {
status string
path []string
}
s := ui.Local(c.Root(), "116", func() state { return state{status: "Ready"} })
c1Section(c, "Treemap", func() {}, func() {
frame, show := c1Frame(c, "Rooms", &s.status)
root := c1Abbey
if !show {
root = mujica.ChartNode{Name: "Abbey"}
s.path = nil
}
r := mujica.Treemap(c, &s.path, root, mujica.TreemapOptions{Frame: frame})
c1Log(log, "Treemap", r)
if r.ValueChanged() {
log("Treemap: path /" + strings.Join(s.path, "/"))
}
})
}
func c1SunburstDemo(c *ui.Context, log func(string)) {
type state struct {
status string
path []string
depth float64
}
s := ui.Local(c.Root(), "117", func() state { return state{status: "Ready", depth: 2} })
c1Section(c, "Sunburst", func() {
ui.Text(c, fmt.Sprintf("%.0f rings", s.depth))
ui.StepSlider(c, &s.depth, 1, 3, 1).Width(120)
}, func() {
frame, show := c1Frame(c, "Rooms", &s.status)
frame.Height = 300
root := c1Abbey
if !show {
root = mujica.ChartNode{Name: "Abbey"}
s.path = nil
}
r := mujica.Sunburst(c, &s.path, root, mujica.SunburstOptions{Frame: frame, Depth: int(s.depth)})
c1Log(log, "Sunburst", r)
if r.ValueChanged() {
log("Sunburst: path /" + strings.Join(s.path, "/"))
}
})
}
func c1SankeyDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "118", func() state { return state{status: "Ready"} })
c1Section(c, "SankeyChart", func() {}, func() {
frame, show := c1Frame(c, "Money", &s.status)
nodes := []mujica.SankeyNode{{ID: "tithe", Label: "Tithes"}, {ID: "alms", Label: "Alms"}, {ID: "fund", Label: "Fund"}, {ID: "roof", Label: "Roof"}, {ID: "bread", Label: "Bread"}, {ID: "bells", Label: "Bells"}}
links := []mujica.SankeyLink{{From: "tithe", To: "fund", Value: 60}, {From: "alms", To: "fund", Value: 30}, {From: "alms", To: "bread", Value: 10}, {From: "fund", To: "roof", Value: 50}, {From: "fund", To: "bells", Value: 40}}
if !show {
links = nil
}
c1Log(log, "SankeyChart", mujica.SankeyChart(c, nodes, links, mujica.SankeyChartOptions{Frame: frame}))
})
}
func c1NetworkDemo(c *ui.Context, log func(string)) {
type state struct {
status string
force bool
graph mujica.NetworkGraphState
}
s := ui.Local(c.Root(), "119", func() state { return state{status: "Ready", force: true} })
c1Section(c, "NetworkGraph", func() {
ui.Checkbox(c, &s.force, "Force layout")
if mujica.Button(c, "Reset view", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.graph.ResetView()
log("NetworkGraph: reset view")
}
}, func() {
frame, show := c1Frame(c, "House", &s.status)
nodes := []mujica.GraphNode{{ID: "a", Label: "Abbot", X: 0, Y: 0}, {ID: "b", Label: "Prior", X: 10, Y: 0}, {ID: "c", Label: "Cellarer", X: 10, Y: 10}, {ID: "d", Label: "Novice", X: 0, Y: 10}, {ID: "e", Label: "Cantor", X: 5, Y: 5}}
edges := []mujica.GraphEdge{{From: "a", To: "b"}, {From: "a", To: "c"}, {From: "c", To: "d"}, {From: "b", To: "e"}, {From: "e", To: "d"}}
if !show {
nodes, edges = nil, nil
}
c1Log(log, "NetworkGraph", mujica.NetworkGraph(c, &s.graph, nodes, edges, mujica.NetworkGraphOptions{Frame: frame, Force: s.force}))
})
}
func c1RealtimeDemo(c *ui.Context, log func(string)) {
type state struct {
status string
feed bool
buf *mujica.RealtimeBuffer
next time.Time
tick int
view mujica.RealtimeChartState
}
s := ui.Local(c.Root(), "120", func() state {
return state{status: "Ready", feed: true, buf: mujica.NewRealtimeBuffer(600, "Bell", "Organ")}
})
// The demo appends a sample every 500 ms while feeding.
if s.feed && !c.Now().Before(s.next) {
t := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC).Add(time.Duration(s.tick) * 500 * time.Millisecond)
x := float64(s.tick)
s.buf.Append(t, 5+3*math.Sin(x/6), 4+2*math.Sin(x/4+1))
s.tick++
s.next = c.Now().Add(500 * time.Millisecond)
}
if s.feed {
c.After(s.next.Sub(c.Now()))
}
c1Section(c, "RealtimeChart", func() { ui.Checkbox(c, &s.feed, "Feed data") }, func() {
frame, _ := c1Frame(c, "Sound", &s.status)
r := mujica.RealtimeChart(c, &s.view, s.buf, mujica.RealtimeChartOptions{Frame: frame, Window: 20 * time.Second})
c1Log(log, "RealtimeChart", r)
if r.ValueChanged() {
log(fmt.Sprintf("RealtimeChart: paused=%v", s.view.Paused))
}
})
}
+197
View File
@@ -0,0 +1,197 @@
package main
import (
"fmt"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func c1Series(gap bool) []mujica.ChartSeries {
a := []float64{3, 5, 4, 6, 8, 7, 9, 11}
if gap {
a = []float64{3, 5, math.NaN(), 6, 8, 7, 9, 11}
}
return []mujica.ChartSeries{
{Name: "Matins", Values: a},
{Name: "Vespers", Values: []float64{2, 3, 5, 4, 6, 9, 8, 7}},
{Name: "Compline", Values: []float64{1, 2, 2, 3, 2, 4, 5, 4}},
}
}
func c1LineDemo(c *ui.Context, log func(string)) {
type state struct {
status string
gap, time bool
big bool
}
s := ui.Local(c.Root(), "101", func() state { return state{status: "Ready", gap: true} })
c1Section(c, "LineChart", func() {
ui.Checkbox(c, &s.gap, "Missing value")
ui.Checkbox(c, &s.time, "Time axis")
ui.Checkbox(c, &s.big, "100,000 points")
}, func() {
frame, show := c1Frame(c, "Attendance", &s.status)
series, opts := c1Series(s.gap), mujica.LineChartOptions{Frame: frame}
if s.big {
series = []mujica.ChartSeries{{Name: "Wave", Values: c1Wave()}}
}
if s.time {
t0 := time.Date(2026, 10, 5, 6, 0, 0, 0, time.UTC)
opts.Times = make([]time.Time, len(series[0].Values))
for i := range opts.Times {
opts.Times[i] = t0.Add(time.Duration(i) * time.Hour)
}
}
if !show {
series = nil
opts.Times = nil
}
c1Log(log, "LineChart", mujica.LineChart(c, series, opts))
})
}
var c1WaveData []float64
// c1Wave is 100,000 points, made once.
func c1Wave() []float64 {
if c1WaveData == nil {
c1WaveData = make([]float64, 100_000)
for i := range c1WaveData {
c1WaveData[i] = math.Sin(float64(i)/900) + 0.3*math.Sin(float64(i)/37)
}
}
return c1WaveData
}
func c1AreaDemo(c *ui.Context, log func(string)) {
type state struct {
status string
stacked bool
base float64
}
s := ui.Local(c.Root(), "102", func() state { return state{status: "Ready", stacked: true} })
c1Section(c, "AreaChart", func() {
ui.Checkbox(c, &s.stacked, "Stacked")
ui.Text(c, fmt.Sprintf("Baseline %.0f", s.base))
ui.StepSlider(c, &s.base, -4, 4, 1).Width(140)
}, func() {
frame, show := c1Frame(c, "Attendance", &s.status)
series := c1Series(false)
if !show {
series = nil
}
c1Log(log, "AreaChart", mujica.AreaChart(c, series, mujica.AreaChartOptions{Frame: frame, Stacked: s.stacked, Baseline: s.base}))
})
}
func c1BarDemo(c *ui.Context, log func(string)) {
type state struct {
status string
horizontal, stacked, labels bool
}
s := ui.Local(c.Root(), "103", func() state { return state{status: "Ready", labels: true} })
c1Section(c, "BarChart", func() {
ui.Checkbox(c, &s.horizontal, "Horizontal")
ui.Checkbox(c, &s.stacked, "Stacked")
ui.Checkbox(c, &s.labels, "Labels")
}, func() {
frame, show := c1Frame(c, "Tithes", &s.status)
cats := []string{"Nave", "Choir", "Crypt", "Bells"}
series := []mujica.ChartSeries{{Name: "Gold", Values: []float64{4, -2, 3, 5}}, {Name: "Silver", Values: []float64{2, 5, 1, -1}}}
if !show {
series = nil
}
c1Log(log, "BarChart", mujica.BarChart(c, cats, series, mujica.BarChartOptions{Frame: frame, Horizontal: s.horizontal, Stacked: s.stacked, Labels: s.labels}))
})
}
func c1PieDemo(c *ui.Context, log func(string)) {
type state struct {
status string
donut bool
min float64
}
s := ui.Local(c.Root(), "104", func() state { return state{status: "Ready", donut: true, min: 20} })
c1Section(c, "PieChart", func() {
ui.Checkbox(c, &s.donut, "Donut")
ui.Text(c, fmt.Sprintf("Merge under %.0f°", s.min))
ui.StepSlider(c, &s.min, 0, 40, 5).Width(140)
}, func() {
frame, show := c1Frame(c, "Stores", &s.status)
slices := []mujica.ChartSlice{{Label: "Wax", Value: 50}, {Label: "Oil", Value: 30}, {Label: "Myrrh", Value: 12}, {Label: "Salt", Value: 4}, {Label: "Ash", Value: 4}}
if !show {
slices = nil
}
c1Log(log, "PieChart", mujica.PieChart(c, slices, mujica.PieChartOptions{Frame: frame, Donut: s.donut, MinAngle: s.min}))
})
}
func c1ScatterDemo(c *ui.Context, log func(string)) {
type state struct {
status string
bubbles bool
selected []mujica.ScatterKey
}
s := ui.Local(c.Root(), "105", func() state { return state{status: "Ready", bubbles: true} })
c1Section(c, "ScatterChart", func() {
ui.Checkbox(c, &s.bubbles, "Bubbles")
ui.Text(c, fmt.Sprintf("%d selected", len(s.selected)))
}, func() {
frame, show := c1Frame(c, "Choir", &s.status)
groups := []mujica.ScatterGroup{
{Name: "Monks", Points: []mujica.ScatterPoint{{X: 1, Y: 2, Size: 4}, {X: 3, Y: 5, Size: 9}, {X: 6, Y: 4, Size: 1}, {X: 7, Y: 8, Size: 6}}},
{Name: "Nuns", Points: []mujica.ScatterPoint{{X: 2, Y: 7, Size: 16}, {X: 8, Y: 9, Size: 2}, {X: 5, Y: 2, Size: 8}}},
}
if !show {
groups = nil
}
r := mujica.ScatterChart(c, &s.selected, groups, mujica.ScatterChartOptions{Frame: frame, Bubbles: s.bubbles})
c1Log(log, "ScatterChart", r)
if r.ValueChanged() {
log(fmt.Sprintf("ScatterChart: %d selected", len(s.selected)))
}
})
}
func c1RadarDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "106", func() state { return state{status: "Ready"} })
c1Section(c, "RadarChart", func() {}, func() {
frame, show := c1Frame(c, "Skills", &s.status)
series := []mujica.ChartSeries{{Name: "Agnès", Values: []float64{8, 6, 9, 4, 7}}, {Name: "Clare", Values: []float64{5, 9, 6, 8, 3}}}
if !show {
series = nil
}
c1Log(log, "RadarChart", mujica.RadarChart(c, []string{"Voice", "Latin", "Chant", "Organ", "Bells"}, series, mujica.RadarChartOptions{Frame: frame}))
})
}
func c1HeatDemo(c *ui.Context, log func(string)) {
type state struct {
status string
gaps bool
}
s := ui.Local(c.Root(), "107", func() state { return state{status: "Ready", gaps: true} })
c1Section(c, "HeatmapChart", func() { ui.Checkbox(c, &s.gaps, "Empty cells") }, func() {
frame, show := c1Frame(c, "Hours", &s.status)
rows, cols := []string{"Mon", "Tue", "Wed", "Thu"}, []string{"Matins", "Lauds", "Terce", "Vespers", "Compline"}
values := make([][]float64, len(rows))
for r := range rows {
values[r] = make([]float64, len(cols))
for col := range cols {
values[r][col] = float64((r*7 + col*5) % 11)
if s.gaps && (r+col)%6 == 1 {
values[r][col] = math.NaN()
}
}
}
if !show {
rows, values = nil, nil
}
c1Log(log, "HeatmapChart", mujica.HeatmapChart(c, rows, cols, values, mujica.HeatmapChartOptions{Frame: frame}))
})
}
+171
View File
@@ -0,0 +1,171 @@
package main
import (
"fmt"
"math"
"strconv"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func c1CalendarDemo(c *ui.Context, log func(string)) {
type state struct {
status, start string
year int
}
s := ui.Local(c.Root(), "108", func() state { return state{status: "Ready", start: "Monday", year: 2026} })
c1Section(c, "CalendarHeatmap", func() { ui.Select(c, &s.start, []string{"Locale", "Sunday", "Monday", "Saturday"}) }, func() {
frame, show := c1Frame(c, "Prayers", &s.status)
frame.Height = 160
values := map[mujica.Date]float64{}
if show {
d := mujica.Date{Year: s.year, Month: time.January, Day: 1}
for i := range 366 {
if (i*37)%5 != 0 {
values[d.AddDays(i)] = float64((i * 13) % 9)
}
}
}
start := map[string]mujica.WeekStart{"Locale": mujica.WeekStartLocale, "Sunday": mujica.WeekStartSunday, "Monday": mujica.WeekStartMonday, "Saturday": mujica.WeekStartSaturday}[s.start]
r := mujica.CalendarHeatmap(c, &s.year, values, mujica.CalendarHeatmapOptions{Frame: frame, WeekStart: start})
c1Log(log, "CalendarHeatmap", r)
if r.ValueChanged() {
log("CalendarHeatmap: year " + strconv.Itoa(s.year))
}
})
}
func c1HistogramDemo(c *ui.Context, log func(string)) {
type state struct {
status string
bins float64
cumulative bool
values []float64
}
s := ui.Local(c.Root(), "109", func() state {
v := make([]float64, 500)
for i := range v {
v[i] = 50 + 12*math.Sin(float64(i)*1.7)*math.Cos(float64(i)*0.31)
}
return state{status: "Ready", bins: 10, values: v}
})
c1Section(c, "Histogram", func() {
ui.Text(c, fmt.Sprintf("%.0f bins", s.bins))
ui.StepSlider(c, &s.bins, 3, 30, 1).Width(140)
ui.Checkbox(c, &s.cumulative, "Cumulative")
}, func() {
frame, show := c1Frame(c, "Ages", &s.status)
values := s.values
if !show {
values = nil
}
c1Log(log, "Histogram", mujica.Histogram(c, values, mujica.HistogramOptions{Frame: frame, Bins: int(s.bins), Cumulative: s.cumulative}))
})
}
func c1BoxDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "110", func() state { return state{status: "Ready"} })
c1Section(c, "BoxPlot", func() {}, func() {
frame, show := c1Frame(c, "Hymns", &s.status)
groups := []mujica.BoxGroup{
{Name: "Choir", Values: []float64{1, 2, 3, 4, 5, 6, 7, 8, 9, 24}},
{Name: "Nave", Values: []float64{4, 5, 5, 6, 7, 8}},
{Name: "Crypt", Values: []float64{12, 14, 15, 18, 22, 25}},
}
if !show {
groups = nil
}
c1Log(log, "BoxPlot", mujica.BoxPlot(c, groups, mujica.BoxPlotOptions{Frame: frame}))
})
}
func c1WaterfallDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "111", func() state { return state{status: "Ready"} })
c1Section(c, "WaterfallChart", func() {}, func() {
frame, show := c1Frame(c, "Ledger", &s.status)
steps := []mujica.WaterfallStep{
{Label: "Tithes", Value: 12}, {Label: "Repairs", Value: -5}, {Label: "Q1", Kind: mujica.WaterfallSubtotal},
{Label: "Alms", Value: 4}, {Label: "Bread", Value: -2}, {Label: "Year", Kind: mujica.WaterfallTotal},
}
if !show {
steps = nil
}
c1Log(log, "WaterfallChart", mujica.WaterfallChart(c, steps, mujica.WaterfallChartOptions{Frame: frame}))
})
}
func c1FunnelDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "112", func() state { return state{status: "Ready"} })
c1Section(c, "FunnelChart", func() {}, func() {
frame, show := c1Frame(c, "Order", &s.status)
stages := []mujica.ChartSlice{{Label: "Visitors", Value: 1000}, {Label: "Novices", Value: 400}, {Label: "Vows", Value: 100}, {Label: "Priors", Value: 12}}
if !show {
stages = nil
}
c1Log(log, "FunnelChart", mujica.FunnelChart(c, stages, mujica.FunnelChartOptions{Frame: frame}))
})
}
func c1BulletDemo(c *ui.Context, log func(string)) {
type state struct {
status string
value, target float64
vertical bool
}
s := ui.Local(c.Root(), "113", func() state { return state{status: "Ready", value: 270, target: 250} })
c1Section(c, "BulletChart", func() {
ui.Text(c, fmt.Sprintf("Value %.0f", s.value))
ui.StepSlider(c, &s.value, 0, 300, 10).Width(140)
ui.Checkbox(c, &s.vertical, "Vertical")
}, func() {
frame, _ := c1Frame(c, "Alms", &s.status)
if s.vertical {
frame.Height = 200
}
target := s.target
c1Log(log, "BulletChart", mujica.BulletChart(c, s.value, mujica.BulletChartOptions{Frame: frame, Target: &target, Bands: []float64{150, 225, 300}, Vertical: s.vertical}))
})
}
func c1SparkDemo(c *ui.Context, log func(string)) {
type state struct{ status string }
s := ui.Local(c.Root(), "114", func() state { return state{status: "Ready"} })
c1Section(c, "Sparkline", func() {}, func() {
frame, show := c1Frame(c, "Bells", &s.status)
values := []float64{3, 5, 4, math.NaN(), 7, 6, 9, 8, 11}
if !show {
values = nil
}
ui.Row(c).Gap(24).Wrap().Children(func() {
for _, k := range []mujica.SparklineKind{mujica.SparklineLine, mujica.SparklineBar, mujica.SparklineArea} {
f := frame
f.Label = fmt.Sprintf("Bells %d", k+1)
c1Log(log, f.Label, mujica.Sparkline(c, values, mujica.SparklineOptions{Frame: f, Kind: k}))
}
})
})
}
func c1GaugeDemo(c *ui.Context, log func(string)) {
type state struct {
status string
value float64
full bool
}
s := ui.Local(c.Root(), "115", func() state { return state{status: "Ready", value: 72} })
c1Section(c, "Gauge", func() {
ui.Text(c, fmt.Sprintf("Value %.0f", s.value))
ui.StepSlider(c, &s.value, 0, 100, 1).Width(140)
ui.Checkbox(c, &s.full, "Full circle")
}, func() {
frame, _ := c1Frame(c, "Nave", &s.status)
segments := []mujica.GaugeSegment{{To: 60, Label: "Calm", Tone: mujica.GaugeGood}, {To: 85, Label: "Busy", Tone: mujica.GaugeWarning}, {To: 100, Label: "Crowded", Tone: mujica.GaugeDanger}}
c1Log(log, "Gauge", mujica.Gauge(c, s.value, mujica.GaugeOptions{Frame: frame, Max: 100, Unit: "%", Segments: segments, Full: s.full}))
})
}
+71
View File
@@ -0,0 +1,71 @@
package main
import (
"slices"
"strings"
"testing"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
// c1Demo runs page id's demo with an event log.
func c1Demo(t *testing.T, id string) (*ui.Tester, *[]string) {
t.Helper()
i := slices.IndexFunc(charts1Pages, func(p page) bool { return p.ID == id })
if i < 0 {
t.Fatalf("no page %s", id)
}
events := &[]string{}
tt := ui.NewTester(func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
ui.Scroll(c).Fill().Padding(24).Children(func() {
charts1Pages[i].Demo(c, func(s string) { *events = append(*events, s) })
})
}, 900, 760)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
return tt, events
}
// c1Labels names each demo's chart for the keys.
var c1Labels = map[string]string{
"101": "Attendance", "102": "Attendance", "103": "Tithes", "104": "Stores", "105": "Choir", "106": "Skills",
"107": "Hours", "108": "Prayers", "109": "Ages", "110": "Hymns", "111": "Ledger", "112": "Order", "113": "Alms",
"114": "Bells 1", "115": "Nave", "116": "Rooms", "117": "Rooms", "118": "Money", "119": "House", "120": "Sound",
}
func TestCharts1DemosLogReadouts(t *testing.T) {
for _, p := range charts1Pages {
if p.Category != "Charts I" || p.Code == "" || p.Keys == "" {
t.Fatalf("%s: incomplete page", p.ID)
}
tt, events := c1Demo(t, p.ID)
tabUntil(t, tt, c1Labels[p.ID])
tt.Key(0, ui.KeyRight)
if len(*events) == 0 || !strings.HasSuffix((*events)[len(*events)-1], "point 0") {
t.Errorf("%s: keyboard readout not logged: %q", p.ID, *events)
}
}
}
func TestCharts1TreemapDrillLogs(t *testing.T) {
tt, events := c1Demo(t, "116")
tabUntil(t, tt, "Rooms")
tt.Key(0, ui.KeyRight)
tt.Key(0, ui.KeyEnter)
if !slices.Contains(*events, "Treemap: path /Library") {
t.Fatalf("drill not logged: %q", *events)
}
}
func tabUntil(t *testing.T, tt *ui.Tester, label string) {
t.Helper()
for range 40 {
if tt.Focused(label) {
return
}
tt.Key(0, ui.KeyTab)
}
t.Fatalf("Tab never reached %q", label)
}
+259 -1
View File
@@ -1,3 +1,261 @@
package main
var chatmsgPages = []page{}
import (
"fmt"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
const chatmsgCategory = "Chat Messages"
var chatmsgPages = append(append(chatmsgListPages, chatmsgBlockPages...), chatmsgActionPages...)
// chatmsgReplies are the lines the demo assistant sends.
var chatmsgReplies = []string{
"The ball begins at nine, in the Hall of Mirrors.",
"Guests arrive by the **east door**; carriages wait in the *lower court*.",
"Seating follows rank: dukes nearest the throne, then the envoys.",
"Shall I send the invitations tonight?",
}
// chatmsgDemo is a chat whose messages the demo adds.
type chatmsgDemo struct {
ids []string
texts []string
list mujica.MessageListState
next int
sidebar bool
dated bool
}
func newChatmsgDemo(n int) *chatmsgDemo {
d := &chatmsgDemo{sidebar: true, dated: true}
for range n {
d.add()
}
return d
}
func (d *chatmsgDemo) add() {
d.ids = append(d.ids, fmt.Sprintf("msg-%d", d.next))
d.texts = append(d.texts, chatmsgReplies[d.next%len(chatmsgReplies)])
d.next++
}
// day puts three messages on each day, ending today.
func (d *chatmsgDemo) day(i int) time.Time {
last := (len(d.ids) - 1) / 3
return time.Now().AddDate(0, 0, i/3-last)
}
// row shows message i as a bubble, users and the assistant taking turns.
func (d *chatmsgDemo) row(c *ui.Context, i int) {
role := mujica.MessageAssistant
if i%2 == 0 {
role = mujica.MessageUser
}
mujica.MessageBubble(c, role, mujica.MessageBubbleOptions{}, func() { mujica.MarkdownView(c, d.texts[i]) })
}
// chatmsgControls adds messages to a demo chat and logs it.
func chatmsgControls(c *ui.Context, log func(string), d *chatmsgDemo) {
ui.Row(c).Gap(8).Children(func() {
if mujica.Button(c, "Add message", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
d.add()
log(fmt.Sprintf("message %s added", d.ids[len(d.ids)-1]))
}
if mujica.Button(c, "Add 10", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
for range 10 {
d.add()
}
log(fmt.Sprintf("10 messages added, %d in all", len(d.ids)))
}
if mujica.Button(c, "Jump to latest", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() {
d.list.ScrollToEnd()
log("scrolled to end")
}
})
}
var chatmsgListPages = []page{
{ID: "201", Name: "ChatContainer", ZhName: "对话容器", Category: chatmsgCategory,
Keys: "Tab 在侧栏、消息与输入区之间移动;消息区滚轮与 PageUp/PageDown 滚动;上翻后出现回到最新按钮。",
Code: `mujica.ChatContainer(c, &chat, len(msgs), mujica.ChatContainerOptions{
List: mujica.MessageListOptions{ID: func(i int) string { return msgs[i] }},
Input: func() { ui.Text(c, "Composer goes here") },
}, func(i int) { ui.Text(c, msgs[i]) })`,
Demo: func(c *ui.Context, log func(string)) {
d := *ui.Local(c.Root(), "201", func() *chatmsgDemo { return newChatmsgDemo(6) })
draft := ui.Local(c.Root(), "201.draft", func() string { return "" })
inputSection(c, "Parameters", func() {
inputFlags(c, log, map[string]*bool{"Sidebar": &d.sidebar}, "Sidebar")
chatmsgControls(c, log, d)
})
inputSection(c, "Live", func() {
ui.Box(c).Height(420).Border(1, mujica.Tokens(c).Border).Clip().Children(func() {
opts := mujica.ChatContainerOptions{
List: mujica.MessageListOptions{ID: func(i int) string { return d.ids[i] }},
Input: func() {
r := mujica.TextInput(c, draft, mujica.TextInputOptions{Label: "Message", Placeholder: "Write to the herald"})
if r.Submitted() && strings.TrimSpace(*draft) != "" {
d.add()
d.texts[len(d.texts)-1] = *draft
log("sent a message")
*draft = ""
}
},
}
if d.sidebar {
opts.Sidebar = func() {
ui.Column(c).Padding(12).Gap(8).Children(func() {
for _, s := range []string{"The ball", "Seating", "Envoys"} {
ui.Text(c, s)
}
})
}
}
r := mujica.ChatContainer(c, &d.list, len(d.ids), opts, func(i int) { d.row(c, i) })
if r.Unread() > 0 {
ui.Text(c, fmt.Sprintf("%d unread", r.Unread())).Absolute().Top(8).Right(12)
}
})
})
}},
{ID: "202", Name: "MessageList", ZhName: "消息列表", Category: chatmsgCategory,
Keys: "滚轮与方向键滚动;上翻后停止跟随;Tab 到回到最新按钮,Enter 回到底部。",
Code: `r := mujica.MessageList(c, &list, len(ids), mujica.MessageListOptions{
ID: func(i int) string { return ids[i] },
}, func(i int) { ui.Text(c, "Message "+ids[i]) })
fmt.Println("unread", r.Unread())`,
Demo: func(c *ui.Context, log func(string)) {
d := *ui.Local(c.Root(), "202", func() *chatmsgDemo { return newChatmsgDemo(30) })
inputSection(c, "Parameters", func() {
inputFlags(c, log, map[string]*bool{"Date separators": &d.dated}, "Date separators")
chatmsgControls(c, log, d)
ui.Row(c).Children(func() {
if mujica.Button(c, "Load 100,000", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
for len(d.ids) < 100000 {
d.add()
}
log("100,000 messages loaded")
}
})
})
inputSection(c, "Live", func() {
opts := mujica.MessageListOptions{ID: func(i int) string { return d.ids[i] }, Label: func(i int) string { return d.texts[i] }}
if d.dated {
opts.Date = d.day
}
ui.Column(c).Height(360).Border(1, mujica.Tokens(c).Border).Children(func() {
r := mujica.MessageList(c, &d.list, len(d.ids), opts, func(i int) { d.row(c, i) })
ui.Text(c, fmt.Sprintf("%d messages, %d unread, at end %v", len(d.ids), r.Unread(), d.list.AtEnd())).Padding(8)
})
})
}},
{ID: "203", Name: "MessageBubble", ZhName: "消息气泡", Category: chatmsgCategory,
Keys: "气泡不获取焦点;内容中的链接与按钮按 Tab 顺序访问。",
Code: `mujica.MessageBubble(c, mujica.MessageAssistant, mujica.MessageBubbleOptions{Name: "Herald"}, func() {
ui.Text(c, "The ball begins at nine.")
})`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
role mujica.MessageRole
avatar, named bool
width float64
}
s := ui.Local(c.Root(), "203", func() state { return state{role: mujica.MessageAssistant, avatar: true, named: true, width: 360} })
roles := []mujica.RadioItem[mujica.MessageRole]{{Value: mujica.MessageUser, Label: "User"}, {Value: mujica.MessageAssistant, Label: "Assistant"},
{Value: mujica.MessageSystem, Label: "System"}, {Value: mujica.MessageError, Label: "Error"}}
inputSection(c, "Parameters", func() {
if mujica.RadioGroup(c, &s.role, roles, mujica.RadioGroupOptions{Horizontal: true, Label: "Role"}).Changed() {
log(fmt.Sprintf("role %d", s.role))
}
inputFlags(c, log, map[string]*bool{"Avatar": &s.avatar, "Name": &s.named}, "Avatar", "Name")
if mujica.NumberInput(c, &s.width, mujica.NumberInputOptions{Min: 160, Max: 640, Step: 40, Label: "MaxWidth"}).Changed() {
log(fmt.Sprintf("MaxWidth %v", s.width))
}
})
inputSection(c, "Live", func() {
opts := mujica.MessageBubbleOptions{MaxWidth: float32(s.width)}
if s.named {
opts.Name = "Herald"
}
if s.avatar {
opts.Avatar = func() { mujica.Avatar(c, "Herald", mujica.AvatarOptions{}) }
}
mujica.MessageBubble(c, s.role, opts, func() { ui.Text(c, chatmsgReplies[0]+" "+chatmsgReplies[2]) })
})
inputSection(c, "States", func() {
for _, r := range roles {
mujica.MessageBubble(c, r.Value, mujica.MessageBubbleOptions{Name: r.Label}, func() { ui.Text(c, r.Label+" message") })
}
})
}},
{ID: "204", Name: "MessageHeader", ZhName: "消息头", Category: chatmsgCategory, Keys: "不获取焦点。",
Code: `mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: mujica.MessageSent})`,
Demo: func(c *ui.Context, log func(string)) {
status := ui.Local(c.Root(), "204", func() mujica.MessageStatus { return mujica.MessageSent })
items := []mujica.RadioItem[mujica.MessageStatus]{{Value: mujica.MessageNoStatus, Label: "None"}, {Value: mujica.MessageSending, Label: "Sending"},
{Value: mujica.MessageStreaming, Label: "Streaming"}, {Value: mujica.MessageSent, Label: "Sent"}, {Value: mujica.MessageFailed, Label: "Failed"}}
inputSection(c, "Parameters", func() {
if mujica.RadioGroup(c, status, items, mujica.RadioGroupOptions{Horizontal: true, Label: "Status"}).Changed() {
log(fmt.Sprintf("status %d", *status))
}
})
inputSection(c, "Live", func() {
mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: *status})
})
inputSection(c, "States", func() {
for _, it := range items[1:] {
mujica.MessageHeader(c, mujica.MessageHeaderOptions{Model: "herald-2", Time: time.Now(), Status: it.Value})
}
})
}},
{ID: "205", Name: "StreamingText", ZhName: "流式文本", Category: chatmsgCategory, Keys: "不获取焦点;减少动画时直接显示全部文本。",
Code: `mujica.StreamingText(c, reply, mujica.StreamingTextOptions{Streaming: true, Smooth: true})`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
text string
smooth, streaming, auto bool
rate float64
at time.Time
}
s := ui.Local(c.Root(), "205", func() state { return state{smooth: true, streaming: true, rate: 80} })
const full = "By order of the Queen, the ball begins at nine. Guests arrive by the east door, and carriages wait in the lower court until midnight."
inputSection(c, "Parameters", func() {
inputFlags(c, log, map[string]*bool{"Smooth": &s.smooth, "Streaming cursor": &s.streaming, "Stream": &s.auto}, "Smooth", "Streaming cursor", "Stream")
if mujica.NumberInput(c, &s.rate, mujica.NumberInputOptions{Min: 10, Max: 400, Step: 10, Label: "Rate"}).Changed() {
log(fmt.Sprintf("Rate %v", s.rate))
}
ui.Row(c).Children(func() {
if mujica.Button(c, "Restart", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.text = ""
log("stream restarted")
}
})
})
// The demo's own source appends a word every 150 ms while streaming.
if s.auto && len(s.text) < len(full) && c.Now().Sub(s.at) > 150*time.Millisecond {
rest := full[len(s.text):]
n := strings.IndexByte(rest[1:], ' ') + 2
if n <= 1 {
n = len(rest)
}
s.text += rest[:n]
s.at = c.Now()
if len(s.text) == len(full) {
log("stream finished")
}
}
if s.auto && len(s.text) < len(full) {
c.After(150 * time.Millisecond)
}
inputSection(c, "Live", func() {
mujica.StreamingText(c, s.text, mujica.StreamingTextOptions{Streaming: s.streaming && len(s.text) < len(full), Smooth: s.smooth, Rate: s.rate})
})
}},
}
+292
View File
@@ -0,0 +1,292 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
var chatmsgActionPages = []page{
{ID: "211", Name: "AudioMessage", ZhName: "语音消息", Category: chatmsgCategory, Keys: "Tab 到播放按钮与波形;波形上 Left/Right 跳转,Home、End 到首尾。",
Code: `r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: time.Minute, Position: pos})
if d, ok := r.Seek(); ok {
pos = d
}`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
pos time.Duration
playing bool
at time.Time
}
s := ui.Local(c.Root(), "211", func() state { return state{pos: 12 * time.Second} })
const length = 47 * time.Second
// The demo plays: it moves the position while playing.
if s.playing {
s.pos += c.Now().Sub(s.at)
if s.pos >= length {
s.pos, s.playing = length, false
log("playback ended")
}
c.After(100 * time.Millisecond)
}
s.at = c.Now()
inputSection(c, "Live", func() {
r := mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: length, Position: s.pos, Playing: s.playing, Waveform: chatmsgWave})
if r.Toggled() {
s.playing = !s.playing
if s.pos >= length {
s.pos = 0
}
log(fmt.Sprintf("playing %v", s.playing))
}
if d, ok := r.Seek(); ok {
s.pos = d
log(fmt.Sprintf("seek to %v", d.Round(time.Second)))
}
})
inputSection(c, "States", func() {
ui.Column(c).Disabled(true).Children(func() {
mujica.AudioMessage(c, mujica.AudioMessageOptions{Duration: length, Waveform: chatmsgWave})
})
})
}},
{ID: "212", Name: "LinkPreviewCard", ZhName: "链接预览卡片", Category: chatmsgCategory, Keys: "Tab 聚焦卡片,Enter 或 Space 打开。",
Code: `p := mujica.LinkPreview{URL: "https://example.com", Title: "The Palace", Site: "example.com"}
if mujica.LinkPreviewCard(c, p).Clicked() {
fmt.Println("open", p.URL)
}`,
Demo: func(c *ui.Context, log func(string)) {
thumb := ui.Local(c.Root(), "212", func() bool { return true })
inputSection(c, "Parameters", func() {
inputFlags(c, log, map[string]*bool{"Thumbnail": thumb}, "Thumbnail")
})
p := mujica.LinkPreview{URL: "https://example.com/versailles", Title: "The Palace of Versailles", Site: "example.com",
Description: "A royal château in the Île-de-France region, seat of the French court from 1682 until the Revolution."}
if *thumb {
p.Thumbnail = chatmsgPicture("#AD9164")
}
inputSection(c, "Live", func() {
if mujica.LinkPreviewCard(c, p).Clicked() {
log("open " + p.URL)
}
if mujica.LinkPreviewCard(c, mujica.LinkPreview{URL: "https://example.com/hall", Title: "Hall of Mirrors"}).Clicked() {
log("open https://example.com/hall")
}
})
}},
{ID: "213", Name: "QuoteReply", ZhName: "引用回复", Category: chatmsgCategory, Keys: "Tab 聚焦,Enter 或 Space 跳转。",
Code: `if mujica.QuoteReply(c, "Marie", "Let them eat cake.").Clicked() {
fmt.Println("jump to the quoted message")
}`,
Demo: func(c *ui.Context, log func(string)) {
inputSection(c, "Live", func() {
for _, q := range [][2]string{{"Marie", "Let them eat cake."}, {"Herald", chatmsgMarkdown}} {
if mujica.QuoteReply(c, q[0], q[1]).Clicked() {
log("jump to " + q[0] + ": " + chatmsgSnippet(q[1], 24))
}
}
})
}},
{ID: "214", Name: "MessageActions", ZhName: "消息操作", Category: chatmsgCategory, Keys: "Tab 进入工具栏,Left/Right 移动,Enter 或 Space 触发。",
Code: `if a, ok := mujica.MessageActions(c, mujica.MessageAssistant, mujica.MessageActionsOptions{Feedback: &rating}).Action(); ok {
fmt.Println("action", a)
}`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
role mujica.MessageRole
rating mujica.MessageFeedback
rated bool
}
s := ui.Local(c.Root(), "214", func() state { return state{role: mujica.MessageAssistant, rated: true} })
names := map[mujica.MessageAction]string{mujica.ActionCopy: "copy", mujica.ActionRegenerate: "regenerate", mujica.ActionEdit: "edit",
mujica.ActionLike: "like", mujica.ActionDislike: "dislike", mujica.ActionSpeak: "speak", mujica.ActionShare: "share"}
inputSection(c, "Parameters", func() {
roles := []mujica.RadioItem[mujica.MessageRole]{{Value: mujica.MessageUser, Label: "User"}, {Value: mujica.MessageAssistant, Label: "Assistant"},
{Value: mujica.MessageSystem, Label: "System"}, {Value: mujica.MessageError, Label: "Error"}}
if mujica.RadioGroup(c, &s.role, roles, mujica.RadioGroupOptions{Horizontal: true, Label: "Role"}).Changed() {
log(fmt.Sprintf("role %d", s.role))
}
inputFlags(c, log, map[string]*bool{"Rating": &s.rated}, "Rating")
})
inputSection(c, "Live", func() {
opts := mujica.MessageActionsOptions{}
if s.rated {
opts.Feedback = &s.rating
}
if a, ok := mujica.MessageActions(c, s.role, opts).Action(); ok {
log(fmt.Sprintf("action %s, rating %d", names[a], s.rating))
}
})
}},
{ID: "215", Name: "MessageEditor", ZhName: "消息编辑器", Category: chatmsgCategory, Keys: "Cmd/Ctrl+Enter 保存,Escape 取消。",
Code: `if mujica.MessageEditor(c, &draft, mujica.MessageEditorOptions{Original: "Hello"}).Saved() {
fmt.Println("new branch from", draft)
}`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
original, draft string
editing bool
branches []string
index int
}
s := ui.Local(c.Root(), "215", func() state {
return state{original: "Seat the ambassador.", branches: []string{"Seat the ambassador."}}
})
inputSection(c, "Live", func() {
if !s.editing {
mujica.MessageBubble(c, mujica.MessageUser, mujica.MessageBubbleOptions{}, func() { ui.Text(c, s.branches[s.index]) })
ui.Row(c).Gap(8).Justify(ui.End).Children(func() {
if mujica.BranchNavigator(c, &s.index, len(s.branches)).Changed() {
log(fmt.Sprintf("branch %d", s.index+1))
}
if mujica.Button(c, "Edit", mujica.ButtonOptions{Variant: mujica.Ghost}).Clicked() {
s.editing, s.draft = true, s.branches[s.index]
log("editing")
}
})
return
}
r := mujica.MessageEditor(c, &s.draft, mujica.MessageEditorOptions{Original: s.branches[s.index]})
if r.Saved() {
s.branches = append(s.branches, s.draft)
s.index, s.editing = len(s.branches)-1, false
log(fmt.Sprintf("saved as branch %d", len(s.branches)))
}
if r.Canceled() {
s.editing = false
log("edit canceled")
}
})
}},
{ID: "216", Name: "BranchNavigator", ZhName: "分支切换", Category: chatmsgCategory, Keys: "Tab 聚焦,Left/Right 切换,Home、End 到首末。",
Code: `if mujica.BranchNavigator(c, &branch, 3).Changed() {
fmt.Println("show branch", branch)
}`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
index int
count float64
disabled bool
}
s := ui.Local(c.Root(), "216", func() state { return state{index: 1, count: 3} })
inputSection(c, "Parameters", func() {
if mujica.NumberInput(c, &s.count, mujica.NumberInputOptions{Min: 1, Max: 9, StepperOnly: true, Label: "Branches"}).Changed() {
s.index = min(s.index, int(s.count)-1)
log(fmt.Sprintf("%v branches", s.count))
}
inputFlags(c, log, map[string]*bool{"Disabled": &s.disabled}, "Disabled")
})
inputSection(c, "Live", func() {
ui.Row(c).Disabled(s.disabled).Children(func() {
if mujica.BranchNavigator(c, &s.index, int(s.count)).Changed() {
log(fmt.Sprintf("branch %d of %v", s.index+1, s.count))
}
})
})
}},
{ID: "217", Name: "ErrorMessage", ZhName: "错误消息", Category: chatmsgCategory, Keys: "Tab 到错误文本(可选择复制)与重试按钮。",
Code: `opts := mujica.ErrorMessageOptions{RetryAt: time.Now().Add(30 * time.Second)}
if mujica.ErrorMessage(c, "429 Too Many Requests", opts).Retried() {
fmt.Println("send again")
}`,
Demo: func(c *ui.Context, log func(string)) {
until := ui.Local(c.Root(), "217", func() time.Time { return time.Time{} })
inputSection(c, "Parameters", func() {
ui.Row(c).Children(func() {
if mujica.Button(c, "Rate limit for 10 s", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
*until = c.Now().Add(10 * time.Second)
log("rate limited for 10 s")
}
})
})
inputSection(c, "Live", func() {
opts := mujica.ErrorMessageOptions{RetryAt: *until}
if mujica.ErrorMessage(c, `429 Too Many Requests: {"type":"rate_limit_error","message":"Number of requests exceeded"}`, opts).Retried() {
log("retry")
}
if mujica.ErrorMessage(c, "dial tcp 10.0.0.7:443: connection refused", mujica.ErrorMessageOptions{Title: "Could not reach the server"}).Retried() {
log("retry connection")
}
})
}},
{ID: "218", Name: "ThinkingBlock", ZhName: "思考过程", Category: chatmsgCategory, Keys: "Tab 聚焦标题,Enter 或 Space 展开与折叠。",
Code: `mujica.ThinkingBlock(c, &open, mujica.ThinkingBlockOptions{Elapsed: 12 * time.Second}, func() {
ui.Text(c, "Weigh the guest list.")
})`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
open, thinking bool
started time.Time
elapsed time.Duration
}
s := ui.Local(c.Root(), "218", func() state { return state{elapsed: 14 * time.Second} })
inputSection(c, "Parameters", func() {
label := "Start thinking"
if s.thinking {
label = "Finish thinking"
}
ui.Row(c).Children(func() {
if mujica.Button(c, label, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
if s.thinking {
s.elapsed = c.Now().Sub(s.started)
} else {
s.started = c.Now()
}
s.thinking = !s.thinking
log(fmt.Sprintf("thinking %v", s.thinking))
}
})
})
inputSection(c, "Live", func() {
opts := mujica.ThinkingBlockOptions{Thinking: s.thinking, Started: s.started, Elapsed: s.elapsed}
if mujica.ThinkingBlock(c, &s.open, opts, func() {
ui.Text(c, "Forty guests, thirty-six seats. The envoys outrank the counts; four counts dine in the antechamber.")
}).Changed() {
log(fmt.Sprintf("open %v", s.open))
}
})
}},
{ID: "219", Name: "ScrollToBottomButton", ZhName: "回到底部按钮", Category: chatmsgCategory, Keys: "Tab 聚焦,Enter 或 Space 回到底部。",
Code: `ui.Box(c).Fill().Children(func() {
if mujica.ScrollToBottomButton(c, !list.AtEnd(), 3).Clicked() {
list.ScrollToEnd()
}
})`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
away bool
unread float64
}
s := ui.Local(c.Root(), "219", func() state { return state{away: true, unread: 3} })
inputSection(c, "Parameters", func() {
inputFlags(c, log, map[string]*bool{"Away from bottom": &s.away}, "Away from bottom")
if mujica.NumberInput(c, &s.unread, mujica.NumberInputOptions{Min: 0, Max: 99, Label: "Unread"}).Changed() {
log(fmt.Sprintf("unread %v", s.unread))
}
})
inputSection(c, "Live", func() {
ui.Box(c).Height(120).Background(mujica.Tokens(c).Surface).Border(1, mujica.Tokens(c).Border).Children(func() {
if mujica.ScrollToBottomButton(c, s.away, int(s.unread)).Clicked() {
s.away, s.unread = false, 0
log("back to the bottom")
}
})
})
}},
{ID: "220", Name: "TypingIndicator", ZhName: "正在输入", Category: chatmsgCategory, Keys: "不获取焦点;减少动画时显示静态文字。",
Code: `mujica.TypingIndicator(c, "Herald")`,
Demo: func(c *ui.Context, log func(string)) {
typing := ui.Local(c.Root(), "220", func() bool { return true })
inputSection(c, "Parameters", func() {
inputFlags(c, log, map[string]*bool{"Typing": typing}, "Typing")
})
inputSection(c, "Live", func() {
if *typing {
mujica.TypingIndicator(c, "Herald")
}
})
}},
}
+198
View File
@@ -0,0 +1,198 @@
package main
import (
"errors"
"fmt"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
const chatmsgMarkdown = "## The Decree\n\nBy order of **the Queen**, *all* guests use `the east door`. See [the protocol](https://example.com/protocol).\n\n" +
"1. Arrive at nine\n2. Bow to the throne\n - dukes first\n\n> Silence in the Hall of Mirrors.\n\n" +
"```python\ndef bow(guest):\n return f\"{guest} bows\" # deeply\n```\n\n| Guest | Seat |\n|---|--:|\n| Marie | 1 |\n| Axel | 14 |\n\n---\n\nVive la reine."
// chatmsgPicture is a flat colored picture for image demos.
func chatmsgPicture(color string) ui.ImageSource {
return ui.MustParseSVG([]byte(fmt.Sprintf(`<svg xmlns="http://www.w3.org/2000/svg" width="240" height="180"><rect width="240" height="180" fill="%s"/><circle cx="120" cy="90" r="40" fill="#FBF7EF" opacity="0.5"/></svg>`, color)))
}
var chatmsgColors = []string{"#7D2034", "#AD9164", "#355E80", "#326247", "#805714", "#6A5B55", "#9C3C53", "#D2C3AF", "#211C1D"}
var chatmsgBlockPages = []page{
{ID: "206", Name: "MarkdownView", ZhName: "Markdown 视图", Category: chatmsgCategory, Keys: "Tab 到链接,Enter 激活;代码块与表格按钮按 Tab 顺序访问。",
Code: `if url, ok := mujica.MarkdownView(c, "# Decree\n\nSee **the guide** at [the court](https://example.com).").Link(); ok {
fmt.Println("open", url)
}`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
src string
stream bool
at time.Time
}
s := ui.Local(c.Root(), "206", func() state { return state{src: chatmsgMarkdown} })
inputSection(c, "Parameters", func() {
ui.Row(c).Children(func() {
if mujica.Button(c, "Stream it again", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.src, s.stream = "", true
log("streaming started")
}
})
mujica.TextArea(c, &s.src, mujica.TextAreaOptions{Label: "Markdown source", MaxHeight: 160})
})
if s.stream && len(s.src) < len(chatmsgMarkdown) && c.Now().Sub(s.at) > 40*time.Millisecond {
s.src = chatmsgMarkdown[:min(len(chatmsgMarkdown), len(s.src)+6)]
s.at = c.Now()
if len(s.src) == len(chatmsgMarkdown) {
s.stream = false
log("streaming finished; finished blocks were never parsed again")
}
}
if s.stream {
c.After(40 * time.Millisecond)
}
inputSection(c, "Live", func() {
if url, ok := mujica.MarkdownView(c, s.src).Link(); ok {
log("link " + url)
}
})
}},
{ID: "207", Name: "CodeBlock", ZhName: "代码块", Category: chatmsgCategory, Keys: "Tab 到换行、复制、折叠按钮;Space 或 Enter 触发。",
Code: `mujica.CodeBlock(c, "func bow() {}", mujica.CodeBlockOptions{Language: "go", LineNumbers: true})`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
lang string
numbers, wrap bool
}
s := ui.Local(c.Root(), "207", func() state { return state{lang: "go", numbers: true} })
samples := map[string]string{
"go": "// greet the court\nfunc greet(name string) int {\n\treturn len(\"Your Majesty, \" + name + \", the ball awaits you in the Hall of Mirrors\") + 42\n}",
"python": "def bow(guest):\n # deeply, always\n return f\"{guest} bows\" * 3",
"sql": "SELECT name, seat FROM guests WHERE rank > 2 ORDER BY seat LIMIT 10; -- the envoys",
}
inputSection(c, "Parameters", func() {
langs := []mujica.RadioItem[string]{{Value: "go", Label: "Go"}, {Value: "python", Label: "Python"}, {Value: "sql", Label: "SQL"}}
if mujica.RadioGroup(c, &s.lang, langs, mujica.RadioGroupOptions{Horizontal: true, Label: "Language"}).Changed() {
log("language " + s.lang)
}
inputFlags(c, log, map[string]*bool{"LineNumbers": &s.numbers, "Wrap at first": &s.wrap}, "LineNumbers", "Wrap at first")
})
inputSection(c, "Live", func() {
ui.Column(c).Key(s.lang + fmt.Sprint(s.wrap)).Children(func() {
if mujica.CodeBlock(c, samples[s.lang], mujica.CodeBlockOptions{Language: s.lang, LineNumbers: s.numbers, Wrap: s.wrap}).Copied() {
log("code copied")
}
})
})
inputSection(c, "States", func() {
mujica.CodeBlock(c, samples["go"], mujica.CodeBlockOptions{Language: "go", Collapsed: true})
})
}},
{ID: "208", Name: "TableBlock", ZhName: "表格块", Category: chatmsgCategory, Keys: "Tab 到复制按钮,Enter 或 Space 复制为 TSV。",
Code: `if mujica.TableBlock(c, []string{"Guest", "Seat"}, [][]string{{"Marie", "1"}}, mujica.TableBlockOptions{}).Copied() {
fmt.Println("copied as TSV")
}`,
Demo: func(c *ui.Context, log func(string)) {
wide := ui.Local(c.Root(), "208", func() bool { return false })
inputSection(c, "Parameters", func() {
inputFlags(c, log, map[string]*bool{"Wide table": wide}, "Wide table")
})
header := []string{"Guest", "Title", "Seat"}
rows := [][]string{{"Marie", "Queen of France", "1"}, {"Axel", "Count of Fersen", "14"}}
if *wide {
header = append(header, "Arrival", "Carriage", "Retinue", "Notes")
for i := range rows {
rows[i] = append(rows[i], "21:00", "Gilded coach", "Twelve", "Seated near the orchestra, away from the draughts")
}
}
inputSection(c, "Live", func() {
ui.Column(c).MaxWidth(520).Children(func() {
if mujica.TableBlock(c, header, rows, mujica.TableBlockOptions{Align: []ui.Align{ui.Start, ui.Start, ui.End}}).Copied() {
log("table copied as TSV")
}
})
})
}},
{ID: "209", Name: "ImageGrid", ZhName: "图片网格", Category: chatmsgCategory, Keys: "Tab 聚焦图片,Enter 打开查看器;查看器内 Left/Right 切换,Escape 关闭。",
Code: `if i, ok := mujica.ImageGrid(c, []mujica.GridImage{{Source: pic, Alt: "Portrait"}}).Retried(); ok {
fmt.Println("reload", i)
}`,
Demo: func(c *ui.Context, log func(string)) {
type state struct {
n float64
loading, fail bool
}
s := ui.Local(c.Root(), "209", func() state { return state{n: 5} })
inputSection(c, "Parameters", func() {
if mujica.NumberInput(c, &s.n, mujica.NumberInputOptions{Min: 1, Max: 9, Label: "Images", StepperOnly: true}).Changed() {
log(fmt.Sprintf("%v images", s.n))
}
inputFlags(c, log, map[string]*bool{"Last loading": &s.loading, "Second failed": &s.fail}, "Last loading", "Second failed")
})
imgs := make([]mujica.GridImage, int(s.n))
for i := range imgs {
imgs[i] = mujica.GridImage{Source: chatmsgPicture(chatmsgColors[i]), Alt: fmt.Sprintf("Portrait %d", i+1)}
}
if s.loading {
imgs[len(imgs)-1] = mujica.GridImage{Alt: "Loading portrait", Loading: true}
}
if s.fail && len(imgs) > 1 {
imgs[1] = mujica.GridImage{Alt: "Lost portrait", Err: errors.New("404")}
}
inputSection(c, "Live", func() {
if i, ok := mujica.ImageGrid(c, imgs).Retried(); ok {
s.fail = false
log(fmt.Sprintf("retry image %d", i))
}
})
}},
{ID: "210", Name: "FileMessage", ZhName: "文件消息", Category: chatmsgCategory, Keys: "Tab 到文件与下载按钮,Enter 或 Space 触发。",
Code: `r := mujica.FileMessage(c, mujica.FileMessageOptions{Name: "treaty.pdf", Size: 2621440})
if r.Downloaded() {
fmt.Println("save treaty.pdf")
}`,
Demo: func(c *ui.Context, log func(string)) {
disabled := ui.Local(c.Root(), "210", func() bool { return false })
inputSection(c, "Parameters", func() {
inputFlags(c, log, map[string]*bool{"Disabled": disabled}, "Disabled")
})
inputSection(c, "Live", func() {
ui.Column(c).Gap(8).Disabled(*disabled).Children(func() {
for _, f := range []mujica.FileMessageOptions{{Name: "treaty.pdf", Size: 2621440}, {Name: "portrait.png", Size: 812000},
{Name: "ledger.zip", Size: 73400320}, {Name: "decree.go", Size: 1840}, {Name: "notes", Size: 12}} {
r := mujica.FileMessage(c, f)
if r.Opened() {
log("open " + f.Name)
}
if r.Downloaded() {
log("download " + f.Name)
}
}
})
})
}},
}
// chatmsgWave is a fixed waveform for the voice demo.
var chatmsgWave = func() []float32 {
w := make([]float32, 48)
for i := range w {
w[i] = float32(0.25 + 0.7*float64((i*37)%11)/10)
if w[i] > 1 {
w[i] = 1
}
}
return w
}()
// chatmsgSnippet trims s to n runes for logs.
func chatmsgSnippet(s string, n int) string {
r := []rune(strings.TrimSpace(s))
if len(r) > n {
return string(r[:n]) + "…"
}
return string(r)
}
+72
View File
@@ -0,0 +1,72 @@
package main
import (
"image/png"
"os"
"path/filepath"
"slices"
"testing"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
// chatmsgDemoTester runs the demo of page id and returns its tester and event log.
func chatmsgDemoTester(t *testing.T, id string, mode mujica.Mode) (*ui.Tester, *[]string) {
t.Helper()
i := slices.IndexFunc(chatmsgPages, func(p page) bool { return p.ID == id })
if i < 0 {
t.Fatalf("no page %s", id)
}
events := &[]string{}
tt := ui.NewTester(func(c *ui.Context) {
mujica.Use(c, mujica.Settings{Mode: mode})
ui.Scroll(c).Fill().Padding(24).Gap(16).Children(func() {
chatmsgPages[i].Demo(c, func(s string) { *events = append(*events, s) })
})
}, 900, 800)
tt.SetPreferences(ui.Preferences{ReduceMotion: true, TextScale: 1})
tt.Frame()
return tt, events
}
func TestChatmsgDemosRender(t *testing.T) {
if len(chatmsgPages) != 20 {
t.Fatalf("%d chat message pages, want 20", len(chatmsgPages))
}
dir := os.Getenv("CHATMSG_DEMO_DIR")
for _, p := range chatmsgPages {
for _, m := range []mujica.Mode{mujica.Light, mujica.Dark} {
tt, _ := chatmsgDemoTester(t, p.ID, m)
if len(tt.Texts()) == 0 {
t.Errorf("%s: demo shows nothing", p.ID)
}
if dir == "" {
continue
}
f, err := os.Create(filepath.Join(dir, p.ID+"-"+m.String()+".png"))
if err != nil {
t.Fatal(err)
}
if err := png.Encode(f, tt.Image()); err != nil {
t.Fatal(err)
}
f.Close()
}
}
}
func TestChatmsgDemosLogEvents(t *testing.T) {
for id, click := range map[string]string{
"202": "Add message", "207": "Copy code", "214": "Regenerate", "216": "Next branch", "217": "Retry",
} {
tt, events := chatmsgDemoTester(t, id, mujica.Light)
if err := tt.Click(click); err != nil {
t.Fatalf("%s: %v", id, err)
}
if len(*events) == 0 {
t.Errorf("%s: clicking %q logged nothing", id, click)
}
}
}
+184 -1
View File
@@ -1,3 +1,186 @@
package main
var chatsessionPages = []page{}
const chatsessionCategory = "Chat Sessions"
var chatsessionPages = []page{
{ID: "241", Name: "ConversationList", ZhName: "会话列表", Category: chatsessionCategory, Demo: chatDemoConversationList,
Keys: "Up/Down choose a conversation, skipping group headers; typing jumps; Enter opens.", Code: `var state mujica.ConversationListState
convs := []mujica.ChatConversation{
{ID: "1", Title: "Court etiquette", Updated: time.Now()},
{ID: "2", Title: "Royal letters", Updated: time.Now().AddDate(0, 0, -30), Pinned: true},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ConversationList(c, &state, convs, mujica.ConversationListOptions{Label: "History"}, nil).Changed() {
log.Println("open", state.Selected)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "242", Name: "ConversationItem", ZhName: "会话条目", Category: chatsessionCategory, Demo: chatDemoConversationItem,
Keys: "Enter opens; F2 renames in place (Enter keeps, Escape cancels); Delete asks to delete; the ellipsis opens the menu.", Code: `conv := mujica.ChatConversation{ID: "1", Title: "Court etiquette", Updated: time.Now()}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
v := mujica.ConversationItem(c, &conv, mujica.ConversationItemOptions{})
if v.Deleted {
log.Println("delete", conv.ID)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "243", Name: "ConversationSearch", ZhName: "会话搜索", Category: chatsessionCategory, Demo: chatDemoConversationSearch,
Keys: "Type to search; Down moves into the results; Up/Down move; Enter opens; Escape clears.", Code: `query := "seat"
hits := []mujica.ConversationSearchHit{{ID: "1", Title: "Ballroom seating plan"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
v := mujica.ConversationSearch(c, &query, hits, mujica.ConversationSearchOptions{})
if v.QueryChanged {
log.Println("search", query)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "244", Name: "ProjectList", ZhName: "项目列表", Category: chatsessionCategory, Demo: chatDemoProjectList,
Keys: "Up/Down move through folders, projects and conversations; Right/Left show or hide a project's conversations; the ellipsis menu moves a conversation.", Code: `var state mujica.ProjectListState
projects := []mujica.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}}
convs := func(id string) []mujica.ChatConversation {
return []mujica.ChatConversation{{ID: "1", Title: "Menu"}}
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if v := mujica.ProjectList(c, &state, projects, convs, mujica.ProjectListOptions{}); v.Moved {
log.Println("move", v.MoveID, "to", v.MoveTo)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "245", Name: "ProjectKnowledgePanel", ZhName: "项目知识面板", Category: chatsessionCategory, Demo: chatDemoProjectKnowledgePanel,
Keys: "Tab reaches Add files and the file list; Up/Down move between remove buttons; Enter removes.", Code: `files := []mujica.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 12000}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
v := mujica.ProjectKnowledgePanel(c, files, mujica.ProjectKnowledgePanelOptions{Capacity: 200000})
if v.Add {
log.Println("pick files")
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "246", Name: "WelcomeScreen", ZhName: "欢迎页", Category: chatsessionCategory, Demo: chatDemoWelcomeScreen,
Keys: "Tab moves through the cards' examples and the prompt slot; Enter picks.", Code: `cards := []mujica.CapabilityCard{{Title: "Write", Examples: []string{"Draft a decree"}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if v := mujica.WelcomeScreen(c, mujica.WelcomeScreenOptions{Greeting: "Good evening", Cards: cards}); v.Example != "" {
log.Println("ask", v.Example)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "247", Name: "SuggestionChips", ZhName: "建议标签", Category: chatsessionCategory, Demo: chatDemoSuggestionChips,
Keys: "Tab enters the chips; Left/Right move; Enter or Space picks.", Code: `input := ""
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SuggestionChips(c, &input, []string{"Plan a ball", "Write a sonnet"}, mujica.SuggestionChipsOptions{})
}
ui.Render(view, 400, 300, 1)`},
{ID: "248", Name: "PromptLibrary", ZhName: "提示模板库", Category: chatsessionCategory, Demo: chatDemoPromptLibrary,
Keys: "Tab moves between categories, search, templates and fields; Up/Down move in a column; Enter picks.", Code: `var state mujica.PromptLibraryState
templates := []mujica.PromptTemplate{{ID: "1", Category: "Letters", Title: "Invitation", Body: "Invite {{guest}}."}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if v := mujica.PromptLibrary(c, &state, templates, mujica.PromptLibraryOptions{}); v.Inserted != "" {
log.Println("insert", v.Inserted)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "249", Name: "CapabilityCards", ZhName: "能力卡片", Category: chatsessionCategory, Demo: chatDemoCapabilityCards,
Keys: "Tab moves between cards; Up/Down move among a card's examples; Enter picks.", Code: `cards := []mujica.CapabilityCard{{Title: "Plan", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball"}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if v := mujica.CapabilityCards(c, cards, mujica.CapabilityCardsOptions{}); v.Example != "" {
log.Println("ask", v.Example)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "250", Name: "FeedbackForm", ZhName: "反馈表单", Category: chatsessionCategory, Demo: chatDemoFeedbackForm,
Keys: "Left/Right move among reasons, Space toggles; Escape cancels.", Code: `var value mujica.FeedbackValue
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.FeedbackForm(c, &value, mujica.FeedbackFormOptions{Rating: mujica.FeedbackDown}).Submitted {
log.Println("reasons", value.Reasons)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "251", Name: "DateSeparator", ZhName: "日期分隔", Category: chatsessionCategory, Demo: chatDemoDateSeparator,
Keys: "Not focusable; read as the date.", Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DateSeparator(c, mujica.DateOf(time.Now()), mujica.DateSeparatorOptions{})
}
ui.Render(view, 400, 300, 1)`},
{ID: "252", Name: "ThinkingIndicator", ZhName: "思考指示", Category: chatsessionCategory, Demo: chatDemoThinkingIndicator,
Keys: "Not focusable; read as a status.", Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ThinkingIndicator(c, mujica.ThinkingIndicatorOptions{Phase: "Searching the archives"})
}
ui.Render(view, 400, 300, 1)`},
{ID: "253", Name: "StopGeneratingButton", ZhName: "停止生成按钮", Category: chatsessionCategory, Demo: chatDemoStopGeneratingButton,
Keys: "Enter or Space stops; Cmd+. (Ctrl+. elsewhere) stops from anywhere.", Code: `generating := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.StopGeneratingButton(c, mujica.StopGeneratingButtonOptions{Generating: generating}).Changed() {
generating = false
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "254", Name: "SystemPromptEditor", ZhName: "系统提示编辑器", Category: chatsessionCategory, Demo: chatDemoSystemPromptEditor,
Keys: "Cmd+S (Ctrl+S elsewhere) saves.", Code: `prompt, saved := "You are helpful.", "You are helpful."
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.SystemPromptEditor(c, &prompt, mujica.SystemPromptEditorOptions{Default: "You are helpful.", Saved: saved}).Save {
saved = prompt
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "255", Name: "CostEstimator", ZhName: "费用估算", Category: chatsessionCategory, Demo: chatDemoCostEstimator,
Keys: "Not focusable; read as the estimated cost.", Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.CostEstimator(c, mujica.CostEstimatorOptions{InputTokens: 12000, OutputTokens: 3400, InputPrice: 3, OutputPrice: 15})
}
ui.Render(view, 400, 300, 1)`},
{ID: "256", Name: "SharedConversationView", ZhName: "分享会话视图", Category: chatsessionCategory, Demo: chatDemoSharedConversationView,
Keys: "Tab reaches Copy link and the messages; message text is selectable.", Code: `msgs := []mujica.SessionMessage{{Role: mujica.SessionUser, Text: "Hello"}, {Role: mujica.SessionAssistant, Text: "Good day."}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.SharedConversationView(c, msgs, mujica.SharedConversationViewOptions{Title: "Greeting", SharedBy: "Ann"}).Changed() {
c.WriteClipboard("https://example.com/share/1")
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "257", Name: "ConversationExport", ZhName: "会话导出", Category: chatsessionCategory, Demo: chatDemoConversationExport,
Keys: "Arrows choose a format and range; Tab reaches Export.", Code: `var state mujica.ConversationExportState
msgs := []mujica.SessionMessage{{Role: mujica.SessionUser, Text: "Hello"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if v := mujica.ConversationExport(c, &state, msgs, mujica.ConversationExportOptions{Title: "Greeting"}); v.Exported {
log.Println(len(v.Content), "bytes to save")
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "258", Name: "RegenerateMenu", ZhName: "重新生成菜单", Category: chatsessionCategory, Demo: chatDemoRegenerateMenu,
Keys: "Enter, Space or Down open; Up/Down move; Right opens the model list; Escape closes.", Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if v := mujica.RegenerateMenu(c, mujica.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet"}, Current: "Opus"}); v.Chosen {
log.Println("retry", v.Choice.Kind, v.Choice.Model, v.Choice.Variant)
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "259", Name: "ArtifactCard", ZhName: "产物卡片", Category: chatsessionCategory, Demo: chatDemoArtifactCard,
Keys: "Enter or Space opens the artifact panel.", Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ArtifactCard(c, mujica.ArtifactCardOptions{Title: "seating.go", Detail: "Go · 120 lines"}).Changed() {
log.Println("open artifact panel")
}
}
ui.Render(view, 400, 300, 1)`},
{ID: "260", Name: "ChatEmptyState", ZhName: "会话空状态", Category: chatsessionCategory, Demo: chatDemoChatEmptyState,
Keys: "Tab reaches the action; Enter runs it.", Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ChatEmptyState(c, mujica.ChatEmptyStateOptions{Kind: mujica.ChatEmptyOffline}).Action {
log.Println("reconnect")
}
}
ui.Render(view, 400, 300, 1)`},
}
+249
View File
@@ -0,0 +1,249 @@
package main
import (
"fmt"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/icons"
)
// chatHistory is the gallery's sample conversations, dated from now.
func chatHistory(now time.Time) []mujica.ChatConversation {
day := 24 * time.Hour
return []mujica.ChatConversation{
{ID: "a", Title: "Court etiquette notes", Updated: now.Add(-2 * time.Hour)},
{ID: "b", Title: "Ballroom seating plan", Updated: now.Add(-day)},
{ID: "c", Title: "Harpsichord repair", Updated: now.Add(-4 * day)},
{ID: "d", Title: "Coronation menu", Updated: now.Add(-40 * day)},
{ID: "e", Title: "Royal letters", Updated: now.Add(-90 * day), Pinned: true},
{ID: "f", Title: "Garden maze", Updated: now.Add(-time.Hour)},
}
}
func chatSampleMessages(now time.Time) []mujica.SessionMessage {
return []mujica.SessionMessage{
{Role: mujica.SessionUser, Text: "How should guests be seated at the banquet?", Time: now.Add(-10 * time.Minute)},
{Role: mujica.SessionAssistant, Text: "Seat by rank: the host at the head, honored guests to the right.", Time: now.Add(-9 * time.Minute)},
{Role: mujica.SessionUser, Author: "Lady Ann", Text: "And the musicians?", Time: now.Add(-5 * time.Minute)},
{Role: mujica.SessionAssistant, Text: "Place the quartet behind a screen near the gallery.", Time: now.Add(-4 * time.Minute)},
}
}
func chatSampleCards() []mujica.CapabilityCard {
return []mujica.CapabilityCard{
{Title: "Write", Description: "Letters, speeches and verse.", Icon: icons.Must("file-text"), Examples: []string{"Draft a royal decree", "Write a sonnet"}},
{Title: "Plan", Description: "Banquets, seating and schedules.", Icon: icons.Must("calendar"), Examples: []string{"Plan a ball for 200 guests"}},
{Title: "Research", Description: "History, heraldry and music.", Icon: icons.Must("search"), Examples: []string{"Explain the minuet"}},
}
}
func chatDemoConversationList(c *ui.Context, log func(string)) {
type state struct {
list mujica.ConversationListState
items []mujica.ChatConversation
many, rich bool
disabled bool
}
s := *ui.Local(c.Root(), "241", func() *state { return &state{items: chatHistory(time.Now())} })
choiceDemo(c, func() {
if mujica.Switch(c, &s.many, "100,000 conversations", mujica.SwitchOptions{}).Changed() {
s.items = chatHistory(time.Now())
if s.many {
now := time.Now()
for i := range 100000 {
s.items = append(s.items, mujica.ChatConversation{ID: fmt.Sprint("n", i), Title: fmt.Sprintf("Archive %d", i), Updated: now.Add(-time.Duration(i) * 7 * time.Minute)})
}
}
log(fmt.Sprintf("conversations = %d", len(s.items)))
}
choiceFlag(c, &s.rich, "ConversationItem rows", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
var row func(conv mujica.ChatConversation)
if s.rich {
row = func(conv mujica.ChatConversation) {
v := mujica.ConversationItem(c, &conv, mujica.ConversationItemOptions{Selected: conv.ID == s.list.Selected})
if v.Opened {
s.list.Selected = conv.ID
log("open " + conv.Title)
}
}
}
v := mujica.ConversationList(c, &s.list, s.items, mujica.ConversationListOptions{Label: "History", Disabled: s.disabled}, row)
v.Element.Width(380).Height(380)
if v.Changed() {
log("select " + s.list.Selected)
}
if v.Submitted() {
log("open " + s.list.Selected)
}
})
}
func chatDemoConversationItem(c *ui.Context, log func(string)) {
type state struct {
convs []mujica.ChatConversation
selected, disabled bool
}
s := *ui.Local(c.Root(), "242", func() *state { return &state{convs: chatHistory(time.Now())[:3]} })
choiceDemo(c, func() {
choiceFlag(c, &s.selected, "Selected", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
ui.Column(c).Width(420).Gap(4).Children(func() {
for i := range s.convs {
conv := &s.convs[i]
ui.Row(c).Key(conv.ID).Children(func() {
v := mujica.ConversationItem(c, conv, mujica.ConversationItemOptions{Selected: s.selected && i == 0, Disabled: s.disabled})
switch {
case v.Opened:
log("open " + conv.Title)
case v.Renamed:
log("rename to " + conv.Title)
case v.PinToggled:
conv.Pinned = !conv.Pinned
log(fmt.Sprintf("pinned %s = %v", conv.Title, conv.Pinned))
case v.Exported:
log("export " + conv.Title)
case v.Deleted:
log("delete " + conv.Title)
}
})
}
})
})
}
func chatDemoConversationSearch(c *ui.Context, log func(string)) {
type state struct {
query, asked string
at time.Time
hits []mujica.ConversationSearchHit
fail, slow bool
disabled bool
}
s := *ui.Local(c.Root(), "243", func() *state { return &state{} })
choiceDemo(c, func() {
choiceFlag(c, &s.slow, "Slow server", log)
choiceFlag(c, &s.fail, "Fail searches", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
loading := s.asked != s.query
if loading && (!s.slow || choiceDue(c, s.at)) {
s.asked, s.hits, loading = s.query, nil, false
for _, conv := range chatHistory(time.Now()) {
if q := strings.ToLower(s.query); q != "" && strings.Contains(strings.ToLower(conv.Title), q) {
s.hits = append(s.hits, mujica.ConversationSearchHit{ID: conv.ID, Title: conv.Title, Snippet: "…mentions " + strings.ToLower(conv.Title) + "…"})
}
}
log(fmt.Sprintf("results for %q: %d", s.query, len(s.hits)))
}
errText := ""
if s.fail && s.query != "" {
errText = "The archive did not answer."
}
ui.Column(c).Width(420).Children(func() {
v := mujica.ConversationSearch(c, &s.query, s.hits, mujica.ConversationSearchOptions{Loading: loading, Error: errText, Disabled: s.disabled, Label: "Search conversations"})
if v.QueryChanged {
s.at = c.Now()
log(fmt.Sprintf("query %q", s.query))
}
if v.Chosen != "" {
log("open " + v.Chosen)
}
if v.Retry {
s.fail, s.asked = false, ""
log("retry")
}
})
})
}
func chatDemoProjectList(c *ui.Context, log func(string)) {
type state struct {
list mujica.ProjectListState
owner map[string]string
disabled bool
}
s := *ui.Local(c.Root(), "244", func() *state {
return &state{owner: map[string]string{"a": "p1", "b": "p1", "c": "p3", "f": "p2"}}
})
projects := []mujica.ChatProject{{ID: "p1", Name: "Coronation", Folder: "Court"}, {ID: "p2", Name: "Gardens", Folder: "Court"}, {ID: "p3", Name: "Music"}}
choiceDemo(c, func() {
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
ui.Row(c).Gap(24).AlignItems(ui.Start).Children(func() {
ui.Column(c).Width(380).Children(func() {
convs := func(id string) []mujica.ChatConversation {
var out []mujica.ChatConversation
for _, conv := range chatHistory(time.Now()) {
if s.owner[conv.ID] == id {
out = append(out, conv)
}
}
return out
}
v := mujica.ProjectList(c, &s.list, projects, convs, mujica.ProjectListOptions{Label: "Projects", Disabled: s.disabled})
switch {
case v.Moved:
s.owner[v.MoveID] = v.MoveTo
log(fmt.Sprintf("move %s to %s", v.MoveID, v.MoveTo))
case v.Changed:
log("project " + s.list.Selected)
case v.Opened != "":
log("open " + v.Opened)
}
})
choiceLabeled(c, "Drag onto a project", func() {
conv := chatHistory(time.Now())[3]
v := mujica.ConversationItem(c, &conv, mujica.ConversationItemOptions{Draggable: true})
v.Element.Width(300)
if v.Opened {
log("open " + conv.Title)
}
})
})
})
}
func chatDemoProjectKnowledgePanel(c *ui.Context, log func(string)) {
type state struct {
files []mujica.KnowledgeFile
next int
small bool
disabled bool
}
s := *ui.Local(c.Root(), "245", func() *state {
return &state{files: []mujica.KnowledgeFile{{ID: "1", Name: "etiquette.pdf", Tokens: 120000}, {ID: "2", Name: "menu.md", Tokens: 30000}}}
})
choiceDemo(c, func() {
choiceFlag(c, &s.small, "Capacity 150,000", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
capacity := 400000
if s.small {
capacity = 150000
}
ui.Column(c).Width(440).Children(func() {
v := mujica.ProjectKnowledgePanel(c, s.files, mujica.ProjectKnowledgePanelOptions{Capacity: capacity, Disabled: s.disabled})
if v.Add {
s.next++
s.files = append(s.files, mujica.KnowledgeFile{ID: fmt.Sprint("n", s.next), Name: fmt.Sprintf("notes-%d.txt", s.next), Tokens: 15000})
log("add (the demo invents a file)")
}
if v.Removed != "" {
for i, f := range s.files {
if f.ID == v.Removed {
s.files = append(s.files[:i:i], s.files[i+1:]...)
log("remove " + f.Name)
break
}
}
}
})
})
}
+341
View File
@@ -0,0 +1,341 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func chatDemoWelcomeScreen(c *ui.Context, log func(string)) {
type state struct {
input string
cards, disabled bool
}
s := *ui.Local(c.Root(), "246", func() *state { return &state{cards: true} })
choiceDemo(c, func() {
choiceFlag(c, &s.cards, "Capability cards", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
var cards []mujica.CapabilityCard
if s.cards {
cards = chatSampleCards()
}
ui.Column(c).Width(860).Children(func() {
v := mujica.WelcomeScreen(c, mujica.WelcomeScreenOptions{Greeting: "Good afternoon", Subtitle: "How may I serve the court?", Cards: cards, Disabled: s.disabled,
Prompts: func() {
if mujica.SuggestionChips(c, &s.input, []string{"Plan a ball", "Write a sonnet"}, mujica.SuggestionChipsOptions{Disabled: s.disabled}).Chosen != "" {
log(fmt.Sprintf("fill %q", s.input))
}
}})
if v.Example != "" {
log(fmt.Sprintf("ask %q", v.Example))
}
})
})
}
func chatDemoSuggestionChips(c *ui.Context, log func(string)) {
type state struct {
input string
send, disabled bool
}
s := *ui.Local(c.Root(), "247", func() *state { return &state{} })
choiceDemo(c, func() {
choiceFlag(c, &s.send, "Send directly", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
v := mujica.SuggestionChips(c, &s.input, []string{"Plan a ball", "Write a sonnet", "Explain the minuet"},
mujica.SuggestionChipsOptions{Send: s.send, Label: "Suggestions", Disabled: s.disabled})
switch {
case v.Sent:
log(fmt.Sprintf("send %q", v.Chosen))
case v.Chosen != "":
log(fmt.Sprintf("fill %q", v.Chosen))
}
ui.Column(c).Width(420).Children(func() {
mujica.TextInput(c, &s.input, mujica.TextInputOptions{Label: "Message", Placeholder: "Message"})
})
})
}
func chatDemoPromptLibrary(c *ui.Context, log func(string)) {
type state struct {
lib mujica.PromptLibraryState
disabled bool
}
s := *ui.Local(c.Root(), "248", func() *state { return &state{} })
templates := []mujica.PromptTemplate{
{ID: "t1", Category: "Letters", Title: "Invitation", Body: "Write an invitation to {{guest}} for the {{event}}."},
{ID: "t2", Category: "Letters", Title: "Thank-you note", Body: "Thank {{host}} for the evening."},
{ID: "t3", Category: "Music", Title: "Recital program", Body: "List pieces for a {{instrument}} recital."},
{ID: "t4", Category: "Planning", Title: "Seating chart", Body: "Seat {{count}} guests by rank."},
}
choiceDemo(c, func() {
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
before := s.lib.Selected
v := mujica.PromptLibrary(c, &s.lib, templates, mujica.PromptLibraryOptions{Label: "Prompt library", Disabled: s.disabled})
if s.lib.Selected != before {
log("preview " + s.lib.Selected)
}
if v.Inserted != "" {
log(fmt.Sprintf("insert %q", v.Inserted))
}
})
}
func chatDemoCapabilityCards(c *ui.Context, log func(string)) {
type state struct {
one, disabled bool
}
s := *ui.Local(c.Root(), "249", func() *state { return &state{} })
choiceDemo(c, func() {
choiceFlag(c, &s.one, "One column", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
cols := 0
if s.one {
cols = 1
}
ui.Column(c).Width(860).Children(func() {
if v := mujica.CapabilityCards(c, chatSampleCards(), mujica.CapabilityCardsOptions{Columns: cols, Disabled: s.disabled}); v.Example != "" {
log(fmt.Sprintf("ask %q", v.Example))
}
})
})
}
func chatDemoFeedbackForm(c *ui.Context, log func(string)) {
type state struct {
value mujica.FeedbackValue
down, submitting bool
disabled bool
at time.Time
}
s := *ui.Local(c.Root(), "250", func() *state { return &state{down: true} })
choiceDemo(c, func() {
choiceFlag(c, &s.down, "Thumbs down", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
rating := mujica.FeedbackUp
if s.down {
rating = mujica.FeedbackDown
}
if s.submitting && choiceDue(c, s.at) {
s.submitting, s.value = false, mujica.FeedbackValue{}
log("feedback sent")
}
ui.Column(c).Width(480).Children(func() {
v := mujica.FeedbackForm(c, &s.value, mujica.FeedbackFormOptions{Rating: rating, Submitting: s.submitting, Disabled: s.disabled})
if v.Submitted {
s.submitting, s.at = true, c.Now()
log(fmt.Sprintf("submit reasons %q, %d characters", s.value.Reasons, len(s.value.Comment)))
}
if v.Cancelled {
s.value = mujica.FeedbackValue{}
log("cancel")
}
})
})
}
func chatDemoDateSeparator(c *ui.Context, log func(string)) {
today := mujica.DateOf(time.Now())
choiceDemo(c, func() {
ui.Text(c, "No parameters; the language follows the window.")
}, func() {
ui.Column(c).Width(520).Children(func() {
for _, d := range []int{0, -1, -12} {
mujica.DateSeparator(c, today.AddDays(d), mujica.DateSeparatorOptions{})
}
})
})
}
func chatDemoThinkingIndicator(c *ui.Context, log func(string)) {
type state struct{ phase int }
s := *ui.Local(c.Root(), "252", func() *state { return &state{} })
phases := []string{"", "Searching the archives", "Reading 3 letters", "Composing a reply"}
choiceDemo(c, func() {
if mujica.Button(c, "Next phase", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
s.phase = (s.phase + 1) % len(phases)
log(fmt.Sprintf("phase %q", phases[s.phase]))
}
}, func() {
mujica.ThinkingIndicator(c, mujica.ThinkingIndicatorOptions{Phase: phases[s.phase]})
})
}
func chatDemoStopGeneratingButton(c *ui.Context, log func(string)) {
type state struct {
generating, disabled bool
at time.Time
}
s := *ui.Local(c.Root(), "253", func() *state { return &state{} })
choiceDemo(c, func() {
if mujica.Button(c, "Generate", mujica.ButtonOptions{Disabled: s.generating}).Clicked() {
s.generating, s.at = true, c.Now()
log("generate")
}
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
if s.generating && c.Now().Sub(s.at) > 8*time.Second {
s.generating = false
log("finished")
} else if s.generating {
c.After(time.Second)
}
if mujica.StopGeneratingButton(c, mujica.StopGeneratingButtonOptions{Generating: s.generating, Disabled: s.disabled}).Changed() {
s.generating = false
log("stop")
}
if !s.generating {
ui.Text(c, "Idle: the button is hidden.").TextColor(mujica.Tokens(c).TextMuted)
}
})
}
func chatDemoSystemPromptEditor(c *ui.Context, log func(string)) {
type state struct {
value, saved string
limit, disabled bool
saving bool
at time.Time
}
s := *ui.Local(c.Root(), "254", func() *state {
return &state{value: "You are a courtly assistant.", saved: "You are a courtly assistant."}
})
choiceDemo(c, func() {
choiceFlag(c, &s.limit, "Limit 60", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
if s.saving && choiceDue(c, s.at) {
s.saving, s.saved = false, s.value
log("saved")
}
limit := 0
if s.limit {
limit = 60
}
ui.Column(c).Width(520).Children(func() {
v := mujica.SystemPromptEditor(c, &s.value, mujica.SystemPromptEditorOptions{Default: "You are helpful.", Saved: s.saved,
MaxLength: limit, Saving: s.saving, Disabled: s.disabled})
if v.Save {
s.saving, s.at = true, c.Now()
log(fmt.Sprintf("save %d characters", len(s.value)))
}
if v.Restored {
log("restore default")
}
})
})
}
func chatDemoCostEstimator(c *ui.Context, log func(string)) {
type state struct{ in, out, inPrice, outPrice float64 }
s := *ui.Local(c.Root(), "255", func() *state { return &state{in: 12000, out: 3400, inPrice: 3, outPrice: 15} })
choiceDemo(c, func() {
for _, f := range []struct {
v *float64
label string
}{{&s.in, "Input tokens"}, {&s.out, "Output tokens"}, {&s.inPrice, "Input $/M"}, {&s.outPrice, "Output $/M"}} {
choiceLabeled(c, f.label, func() {
r := mujica.NumberInput(c, f.v, mujica.NumberInputOptions{Min: 0, Max: 1e7, Step: 100, Label: f.label})
r.Element.Width(150)
if r.Changed() {
log(fmt.Sprintf("%s = %v", f.label, *f.v))
}
})
}
}, func() {
ui.Column(c).Width(420).Children(func() {
mujica.CostEstimator(c, mujica.CostEstimatorOptions{InputTokens: int(s.in), OutputTokens: int(s.out), InputPrice: s.inPrice, OutputPrice: s.outPrice})
})
})
}
func chatDemoSharedConversationView(c *ui.Context, log func(string)) {
type state struct{ disabled bool }
s := *ui.Local(c.Root(), "256", func() *state { return &state{} })
choiceDemo(c, func() {
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
ui.Column(c).Width(600).Height(420).Children(func() {
if mujica.SharedConversationView(c, chatSampleMessages(time.Now()), mujica.SharedConversationViewOptions{Title: "Banquet seating",
SharedBy: "Lady Ann", SharedAt: time.Now(), Disabled: s.disabled}).Changed() {
c.WriteClipboard("https://example.com/share/banquet")
log("copy link")
}
})
})
}
func chatDemoConversationExport(c *ui.Context, log func(string)) {
type state struct {
export mujica.ConversationExportState
disabled bool
}
s := *ui.Local(c.Root(), "257", func() *state { return &state{} })
choiceDemo(c, func() {
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
ui.Column(c).Width(580).Children(func() {
v := mujica.ConversationExport(c, &s.export, chatSampleMessages(time.Now()), mujica.ConversationExportOptions{Title: "Banquet seating",
Selected: []int{1, 3}, Disabled: s.disabled})
if v.Exported {
log(fmt.Sprintf("export %d bytes as format %d (the caller writes the file)", len(v.Content), v.Format))
}
})
})
}
func chatDemoRegenerateMenu(c *ui.Context, log func(string)) {
type state struct{ disabled bool }
s := *ui.Local(c.Root(), "258", func() *state { return &state{} })
choiceDemo(c, func() {
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
v := mujica.RegenerateMenu(c, mujica.RegenerateMenuOptions{Models: []string{"Opus", "Sonnet", "Haiku"}, Current: "Opus", Disabled: s.disabled})
if v.Chosen {
log(fmt.Sprintf("retry kind %d model %q variant %q", v.Choice.Kind, v.Choice.Model, v.Choice.Variant))
}
})
}
func chatDemoArtifactCard(c *ui.Context, log func(string)) {
type state struct{ streaming, disabled bool }
s := *ui.Local(c.Root(), "259", func() *state { return &state{} })
choiceDemo(c, func() {
choiceFlag(c, &s.streaming, "Streaming", log)
choiceFlag(c, &s.disabled, "Disabled", log)
}, func() {
for _, a := range []mujica.ArtifactCardOptions{
{Kind: mujica.ArtifactCode, Title: "seating.go", Detail: "Go · 120 lines"},
{Kind: mujica.ArtifactDocument, Title: "Royal decree", Detail: "2 pages"},
{Kind: mujica.ArtifactWeb, Title: "Ball invitation"},
} {
a.Streaming, a.Disabled = s.streaming, s.disabled
if mujica.ArtifactCard(c, a).Changed() {
log("open " + a.Title)
}
}
})
}
func chatDemoChatEmptyState(c *ui.Context, log func(string)) {
type state struct{ kind int }
s := *ui.Local(c.Root(), "260", func() *state { return &state{} })
choiceDemo(c, func() {
mujica.RadioGroup(c, &s.kind, []mujica.RadioItem[int]{{Value: 0, Label: "New"}, {Value: 1, Label: "No results"}, {Value: 2, Label: "Offline"}},
mujica.RadioGroupOptions{Horizontal: true, Label: "Kind"})
}, func() {
ui.Column(c).Width(420).Children(func() {
if mujica.ChatEmptyState(c, mujica.ChatEmptyStateOptions{Kind: mujica.ChatEmptyKind(s.kind), Query: "minuet"}).Action {
log(fmt.Sprintf("action for kind %d", s.kind))
}
})
})
}
+243 -1
View File
@@ -1,3 +1,245 @@
package main
var codePages = []page{}
import (
"fmt"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
const codeCategory = "Terminal & Code"
var codePages = []page{
{ID: "321", Name: "Terminal", ZhName: "终端", Category: codeCategory, Demo: codeTerminalDemo, Code: codeTerminalCode,
Keys: "Focused: keys and text go to the PTY; Ctrl+letters send control codes; Cmd+C/V (Ctrl+Shift+C/V elsewhere) copy and paste; Shift+PageUp/PageDown scroll back; drag selects."},
{ID: "322", Name: "TerminalTabs", ZhName: "终端标签", Category: codeCategory, Demo: codeTerminalTabsDemo, Code: codeTerminalTabsCode,
Keys: "Left/Right, Home/End switch tabs; F2 or double-click renames (Enter saves, Escape cancels); Delete closes, asking first while a process runs."},
{ID: "323", Name: "TerminalSearch", ZhName: "终端搜索", Category: codeCategory, Demo: codeTerminalSearchDemo, Code: codeTerminalSearchCode,
Keys: "Enter / Shift+Enter go to the next / previous match; Cmd+G (F3 elsewhere) too."},
{ID: "324", Name: "CommandHistory", ZhName: "命令历史", Category: codeCategory, Demo: codeCommandHistoryDemo, Code: codeCommandHistoryCode,
Keys: "Up/Down choose; Enter or double-click replays; typing in the list jumps by command."},
{ID: "325", Name: "LogViewer", ZhName: "日志查看器", Category: codeCategory, Demo: codeLogViewerDemo, Code: codeLogViewerCode,
Keys: "Space toggles a level or Follow; arrows, PageUp/PageDown, Home/End scroll the log."},
{ID: "326", Name: "ProcessList", ZhName: "进程列表", Category: codeCategory, Demo: codeProcessListDemo, Code: codeProcessListCode,
Keys: "Up/Down choose; Delete ends the chosen process after confirming; click a header to sort, again to reverse."},
{ID: "327", Name: "CodeViewer", ZhName: "代码查看器", Category: codeCategory, Demo: codeCodeViewerDemo, Code: codeCodeViewerCode,
Keys: "Up/Down move the current line; Shift extends; Cmd/Ctrl+C copies; Left folds, Right unfolds."},
{ID: "328", Name: "Minimap", ZhName: "代码缩略图", Category: codeCategory, Demo: codeMinimapDemo, Code: codeMinimapCode,
Keys: "Up/Down move a line, PageUp/PageDown a screen, Home/End to the ends; click or drag jumps."},
{ID: "329", Name: "FindWidget", ZhName: "查找替换", Category: codeCategory, Demo: codeFindWidgetDemo, Code: codeFindWidgetCode,
Keys: "Enter / Shift+Enter next / previous match; Enter in the replace field replaces the current match."},
{ID: "330", Name: "SearchPanel", ZhName: "多文件搜索", Category: codeCategory, Demo: codeSearchPanelDemo, Code: codeSearchPanelCode,
Keys: "Up/Down choose; Enter opens a match or folds a file; Left folds, Right unfolds a file."},
{ID: "331", Name: "ProblemsPanel", ZhName: "问题面板", Category: codeCategory, Demo: codeProblemsDemo, Code: codeProblemsPanelCode,
Keys: "Space toggles a severity; Up/Down choose; Enter opens a problem or folds a file; Left/Right fold and unfold."},
{ID: "332", Name: "OutputPanel", ZhName: "输出面板", Category: codeCategory, Demo: codeOutputDemo, Code: codeOutputPanelCode,
Keys: "Tab reaches the channel menu, Follow and Clear; arrows choose a channel in the open menu."},
{ID: "333", Name: "SymbolOutline", ZhName: "符号大纲", Category: codeCategory, Demo: codeSymbolDemo, Code: codeSymbolOutlineCode,
Keys: "Up/Down move; Right opens, Left closes; Enter or double-click jumps; typing jumps by name."},
{ID: "334", Name: "CompletionMenu", ZhName: "补全菜单", Category: codeCategory, Demo: codeCompletionDemo, Code: codeCompletionMenuCode,
Keys: "While typing: Down/Up move, Tab or Enter accepts, Escape closes; the editor keeps the focus."},
{ID: "335", Name: "DebugToolbar", ZhName: "调试工具栏", Category: codeCategory, Demo: codeDebugDemo, Code: codeDebugToolbarCode,
Keys: "F5 continue, F6 pause, F10 step over, F11 step into, Shift+F11 step out, Cmd/Ctrl+Shift+F5 restart, Shift+F5 stop; Left/Right inside the toolbar."},
{ID: "336", Name: "BreakpointList", ZhName: "断点列表", Category: codeCategory, Demo: codeBreakpointDemo, Code: codeBreakpointListCode,
Keys: "Space toggles; Enter on a location opens it; the condition field saves with Enter, cancels with Escape."},
{ID: "337", Name: "CallStack", ZhName: "调用栈", Category: codeCategory, Demo: codeCallStackDemo, Code: codeCallStackCode,
Keys: "Up/Down choose a frame; Enter folds a thread; Left/Right fold and unfold."},
{ID: "338", Name: "VariablesPanel", ZhName: "变量面板", Category: codeCategory, Demo: codeVariablesDemo, Code: codeVariablesPanelCode,
Keys: "Up/Down move; Right opens, Left closes; Enter, F2 or double-click edits a leaf; Enter saves, Escape cancels."},
{ID: "339", Name: "HexViewer", ZhName: "十六进制查看器", Category: codeCategory, Demo: codeHexDemo, Code: codeHexViewerCode,
Keys: "Arrows move by byte and row; Shift extends; Home/End go to the row ends; Cmd/Ctrl+C copies hex."},
{ID: "340", Name: "Flamegraph", ZhName: "火焰图", Category: codeCategory, Demo: codeFlamegraphDemo, Code: codeFlamegraphCode,
Keys: "Up parent, Down first child, Left/Right siblings; Enter zooms in, Escape zooms out; the readout is announced."},
}
// codeDemoCheck is a switch that logs its changes.
func codeDemoCheck(c *ui.Context, v *bool, label string, log func(string)) {
if mujica.Switch(c, v, label, mujica.SwitchOptions{}).Changed() {
log(fmt.Sprintf("%s: %v", label, *v))
}
}
func codeDemoControls(c *ui.Context, fn func()) {
ui.Row(c).Gap(16).Wrap().AlignItems(ui.Center).Margin(0, 0, 16, 0).Children(fn)
}
func codeTerminalDemo(c *ui.Context, log func(string)) {
type state struct {
term mujica.TerminalState
line string
disabled bool
}
s := ui.Local(c.Root(), "321", func() *state {
s := &state{}
s.term.Write("Welcome to the \x1b[1;35mcourt\x1b[0m shell. Type and press Enter.\r\n$ ")
return s
})
st := *s
codeDemoControls(c, func() {
codeDemoCheck(c, &st.disabled, "Disabled", log)
if mujica.Button(c, "Clear screen", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
st.term.Write("\x1b[2J\x1b[H$ ")
log("clear screen")
}
})
// A tiny echo shell stands in for the PTY.
mujica.Terminal(c, &st.term, mujica.TerminalOptions{Disabled: st.disabled,
Input: func(data string) {
log(fmt.Sprintf("input %q", data))
for _, r := range data {
switch r {
case '\r':
st.term.Write("\r\n" + codeShell(st.line) + "$ ")
st.line = ""
case 0x7f:
if st.line != "" {
st.line = st.line[:len(st.line)-1]
st.term.Write("\b \b")
}
default:
if r >= ' ' {
st.line += string(r)
st.term.Write(string(r))
}
}
}
},
Resized: func(cols, rows int) { log(fmt.Sprintf("resized %d×%d", cols, rows)) },
}).Size(640, 260)
}
// codeShell answers a few commands for the demo terminal.
func codeShell(line string) string {
switch f := strings.Fields(line); {
case len(f) == 0:
return ""
case f[0] == "ls":
return "\x1b[34mchronicles\x1b[0m decrees.txt \x1b[32mseal.sh\x1b[0m\r\n"
case f[0] == "echo":
return strings.Join(f[1:], " ") + "\r\n"
case f[0] == "date":
return time.Now().Format(time.RFC1123) + "\r\n"
}
return "\x1b[31m" + line + ": command not found\x1b[0m\r\n"
}
func codeTerminalTabsDemo(c *ui.Context, log func(string)) {
type state struct {
tabs []mujica.TerminalTab
active string
next int
disabled bool
}
s := ui.Local(c.Root(), "322", func() *state {
return &state{tabs: []mujica.TerminalTab{{ID: "1", Title: "zsh"}, {ID: "2", Title: "build", Busy: true}, {ID: "3", Title: "logs"}}, active: "1", next: 4}
})
st := *s
codeDemoControls(c, func() {
codeDemoCheck(c, &st.disabled, "Disabled", log)
if mujica.Button(c, "New terminal", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
id := fmt.Sprint(st.next)
st.next++
st.tabs = append(st.tabs, mujica.TerminalTab{ID: id, Title: "shell " + id})
st.active = id
log("new " + id)
}
})
r := mujica.TerminalTabs(c, &st.tabs, &st.active, mujica.TerminalTabsOptions{Disabled: st.disabled}, func(id string) {
ui.Text(c, "Terminal "+id+" content").TextColor(mujica.Tokens(c).TextMuted)
})
r.Element.Width(560)
if r.Changed {
log("active " + st.active)
}
if r.Renamed != "" {
log("renamed " + r.Renamed)
}
if r.Closed != "" {
log("closed " + r.Closed)
}
}
func codeTerminalSearchDemo(c *ui.Context, log func(string)) {
type state struct {
term mujica.TerminalState
search mujica.TerminalSearchState
disabled bool
}
s := ui.Local(c.Root(), "323", func() *state {
s := &state{}
for i := range 60 {
if i%9 == 4 {
s.term.Write(fmt.Sprintf("[%02d] \x1b[31merror\x1b[0m: seal %d missing\r\n", i, i))
} else {
s.term.Write(fmt.Sprintf("[%02d] ledger page %d copied\r\n", i, i))
}
}
return s
})
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
ui.Column(c).Width(600).Gap(8).Children(func() {
if mujica.TerminalSearch(c, &st.term, &st.search, mujica.TerminalSearchOptions{Disabled: st.disabled}).Changed() {
log(fmt.Sprintf("query %q, match %d", st.search.Query, st.search.Current+1))
}
mujica.Terminal(c, &st.term, mujica.TerminalOptions{}).Height(220)
})
}
func codeCommandHistoryDemo(c *ui.Context, log func(string)) {
type state struct {
entries []mujica.HistoryEntry
s mujica.CommandHistoryState
disabled bool
}
s := ui.Local(c.Root(), "324", func() *state {
at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
var e []mujica.HistoryEntry
for i, cmd := range []string{"git pull", "go build ./...", "go test ./...", "make seal", "ls -la", "git status"} {
e = append(e, mujica.HistoryEntry{Command: cmd, Time: at.Add(time.Duration(i) * time.Minute)})
}
e[3].ExitCode = 2
return &state{entries: e}
})
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
r := mujica.CommandHistory(c, st.entries, &st.s, mujica.CommandHistoryOptions{Disabled: st.disabled})
r.Element.Size(480, 300)
if r.Replayed != nil {
log("replay " + r.Replayed.Command)
st.entries = append(st.entries, mujica.HistoryEntry{Command: r.Replayed.Command, Time: time.Now()})
}
}
func codeLogViewerDemo(c *ui.Context, log func(string)) {
type state struct {
lines []mujica.LogLine
s mujica.LogViewerState
disabled bool
}
s := ui.Local(c.Root(), "325", func() *state {
at := time.Date(2026, 10, 5, 9, 0, 0, 0, time.UTC)
lines := make([]mujica.LogLine, 100_000)
words := []string{"ledger synced", "seal applied", "herald dispatched", "archive rotated"}
for i := range lines {
lines[i] = mujica.LogLine{Time: at.Add(time.Duration(i) * time.Second), Level: mujica.LogLevel(i % 7 % 4), Text: fmt.Sprintf("%s #%d", words[i%4], i)}
}
return &state{lines: lines}
})
st := *s
codeDemoControls(c, func() {
codeDemoCheck(c, &st.disabled, "Disabled", log)
if mujica.Button(c, "Append line", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
st.lines = append(st.lines, mujica.LogLine{Time: time.Now(), Level: mujica.LogWarn, Text: fmt.Sprintf("late seal #%d", len(st.lines))})
log("append")
}
})
r := mujica.LogViewer(c, st.lines, &st.s, mujica.LogViewerOptions{Disabled: st.disabled})
r.Element.Size(720, 360)
if r.Changed() {
log(fmt.Sprintf("hidden %v, follow %v", st.s.Hidden, st.s.Follow))
}
}
+238
View File
@@ -0,0 +1,238 @@
package main
import (
"fmt"
"strings"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func codeSymbolDemo(c *ui.Context, log func(string)) {
type state struct {
s mujica.SymbolOutlineState
cursor float64
disabled bool
}
s := ui.Local(c.Root(), "333", func() *state { return &state{cursor: 15} })
st := *s
codeDemoControls(c, func() {
codeDemoCheck(c, &st.disabled, "Disabled", log)
ui.Text(c, fmt.Sprintf("Cursor line %.0f", st.cursor))
if mujica.Slider(c, &st.cursor, mujica.SliderOptions{Min: 1, Max: 34, Step: 1}).Changed() {
log(fmt.Sprintf("cursor %.0f", st.cursor))
}
})
symbols := []mujica.Symbol{
{Name: "Decree", Kind: mujica.SymbolType, Line: 6, EndLine: 9, Detail: "struct", Children: []mujica.Symbol{
{Name: "ID", Kind: mujica.SymbolField, Line: 7, EndLine: 7, Detail: "int"},
{Name: "Title", Kind: mujica.SymbolField, Line: 8, EndLine: 8, Detail: "string"},
}},
{Name: "Seal", Kind: mujica.SymbolFunction, Line: 13, EndLine: 21, Detail: "(d Decree) (string, error)"},
{Name: "Herald", Kind: mujica.SymbolFunction, Line: 23, EndLine: 31, Detail: "(ds []Decree) int"},
}
r := mujica.SymbolOutline(c, symbols, &st.s, mujica.SymbolOutlineOptions{CursorLine: int(st.cursor), Disabled: st.disabled})
r.Element.Size(420, 320)
if r.Jumped != nil {
log(fmt.Sprintf("jump to %s, line %d", r.Jumped.Name, r.Jumped.Line))
}
}
func codeCompletionDemo(c *ui.Context, log func(string)) {
type state struct {
text string
open bool
s mujica.CompletionMenuState
disabled bool
}
s := ui.Local(c.Root(), "334", func() *state { return &state{} })
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
all := []mujica.Completion{
{Label: "Seal", Kind: mujica.SymbolFunction, Detail: "func(d Decree) (string, error)", Doc: "Seal stamps a decree with the royal wax."},
{Label: "Search", Kind: mujica.SymbolFunction, Detail: "func(q string) []Decree"},
{Label: "Season", Kind: mujica.SymbolConstant, Detail: "= 4"},
{Label: "Herald", Kind: mujica.SymbolFunction, Detail: "func(ds []Decree) int"},
{Label: "Decree", Kind: mujica.SymbolType, Detail: "struct", Doc: "Decree is an order of the court."},
}
word := st.text[strings.LastIndexAny(st.text, " .(")+1:]
var items []mujica.Completion
for _, it := range all {
if word != "" && strings.HasPrefix(strings.ToLower(it.Label), strings.ToLower(word)) {
items = append(items, it)
}
}
ui.Column(c).Width(420).Gap(8).Children(func() {
ui.Text(c, "Type “se” or “de” to see suggestions.").TextColor(mujica.Tokens(c).TextMuted)
in := mujica.TextInput(c, &st.text, mujica.TextInputOptions{Label: "Code", Placeholder: "court.", Disabled: st.disabled})
if in.Changed() {
st.open = len(items) > 0
}
r := mujica.CompletionMenu(c, in.Input, &st.open, items, &st.s, mujica.CompletionMenuOptions{Disabled: st.disabled})
if r.Accepted != nil {
st.text = st.text[:len(st.text)-len(word)] + r.Accepted.Label
log("accept " + r.Accepted.Label)
}
})
}
func codeDebugDemo(c *ui.Context, log func(string)) {
type state struct {
dbg mujica.DebugState
disabled bool
}
s := ui.Local(c.Root(), "335", func() *state { return &state{dbg: mujica.DebugPaused} })
st := *s
names := []string{"Stopped", "Running", "Paused"}
codeDemoControls(c, func() {
codeDemoCheck(c, &st.disabled, "Disabled", log)
ui.Text(c, "State: "+names[st.dbg])
})
a := mujica.DebugToolbar(c, st.dbg, mujica.DebugToolbarOptions{Disabled: st.disabled}).Action
switch a {
case mujica.DebugNone:
return
case mujica.DebugContinue, mujica.DebugRestart:
st.dbg = mujica.DebugRunning
case mujica.DebugPause, mujica.DebugStepOver, mujica.DebugStepInto, mujica.DebugStepOut:
st.dbg = mujica.DebugPaused
case mujica.DebugStop:
st.dbg = mujica.DebugStopped
}
log(fmt.Sprintf("action %d, now %s", a, names[st.dbg]))
}
func codeBreakpointDemo(c *ui.Context, log func(string)) {
type state struct {
bps []mujica.Breakpoint
disabled bool
}
s := ui.Local(c.Root(), "336", func() *state {
return &state{bps: []mujica.Breakpoint{
{ID: "1", File: "court/seal.go", Line: 14, Enabled: true},
{ID: "2", File: "court/ledger.go", Line: 40, Condition: "n > 3"},
{ID: "3", File: "main.go", Line: 9, Enabled: true},
}}
})
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
r := mujica.BreakpointList(c, &st.bps, mujica.BreakpointListOptions{Disabled: st.disabled})
r.Element.Size(480, 240)
for _, e := range []struct{ what, id string }{{"toggled", r.Toggled}, {"condition", r.Edited}, {"open", r.Opened}} {
if e.id != "" {
log(e.what + " " + e.id)
}
}
}
func codeCallStackDemo(c *ui.Context, log func(string)) {
type state struct {
s mujica.CallStackState
disabled bool
}
s := ui.Local(c.Root(), "337", func() *state { return &state{s: mujica.CallStackState{Current: "f1"}} })
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
threads := []mujica.DebugThread{
{ID: "t1", Name: "main", Frames: []mujica.StackFrame{
{ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14},
{ID: "f2", Name: "court.Herald", File: "court/seal.go", Line: 27},
{ID: "f3", Name: "main.main", File: "main.go", Line: 9},
}},
{ID: "t2", Name: "ledger writer", Frames: []mujica.StackFrame{{ID: "f4", Name: "ledger.Flush", File: "ledger/write.go", Line: 51}}},
}
r := mujica.CallStack(c, threads, &st.s, mujica.CallStackOptions{Disabled: st.disabled})
r.Element.Size(440, 300)
if r.Changed() {
log("frame " + st.s.Current)
}
}
func codeVariablesDemo(c *ui.Context, log func(string)) {
type state struct {
vars []mujica.Variable
s mujica.VariablesPanelState
disabled bool
}
s := ui.Local(c.Root(), "338", func() *state {
return &state{vars: []mujica.Variable{
{Name: "d", Type: "court.Decree", Value: "{…}", Children: []mujica.Variable{
{Name: "ID", Value: "42", Type: "int", Changed: true},
{Name: "Title", Value: `"Of seals"`, Type: "string"},
}},
{Name: "i", Value: "2", Type: "int", Changed: true},
{Name: "err", Value: "nil", Type: "error"},
}}
})
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
r := mujica.VariablesPanel(c, st.vars, &st.s, mujica.VariablesPanelOptions{Disabled: st.disabled})
r.Element.Size(440, 260)
if r.Edited != nil {
log(fmt.Sprintf("set %s = %s", r.Edited.Path, r.Edited.Value))
codeDemoSet(st.vars, strings.Split(r.Edited.Path, "."), r.Edited.Value)
}
}
// codeDemoSet writes value at the dotted path.
func codeDemoSet(vars []mujica.Variable, path []string, value string) {
for i := range vars {
if vars[i].Name != path[0] {
continue
}
if len(path) == 1 {
vars[i].Value, vars[i].Changed = value, true
return
}
codeDemoSet(vars[i].Children, path[1:], value)
}
}
func codeHexDemo(c *ui.Context, log func(string)) {
type state struct {
s mujica.HexViewerState
disabled bool
}
s := ui.Local(c.Root(), "339", func() *state { return &state{} })
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
data := []byte(strings.Join(codeDemoSource, "\n"))
r := mujica.HexViewer(c, data, &st.s, mujica.HexViewerOptions{Disabled: st.disabled})
r.Element.Size(720, 300)
if r.Changed() {
a, b := st.s.Selection()
log(fmt.Sprintf("bytes %d..%d", a, b))
}
}
func codeFlamegraphDemo(c *ui.Context, log func(string)) {
type state struct {
s mujica.FlamegraphState
status string
disabled bool
}
s := ui.Local(c.Root(), "340", func() *state { return &state{status: "Ready"} })
st := *s
codeDemoControls(c, func() {
codeDemoCheck(c, &st.disabled, "Disabled", log)
opts := []mujica.SelectOption[string]{{Value: "Ready", Label: "Ready"}, {Value: "Loading", Label: "Loading"}, {Value: "Failed", Label: "Failed"}}
if mujica.Select(c, &st.status, opts, mujica.SelectOptions{Label: "Status"}).Changed() {
log("status " + st.status)
}
})
root := &mujica.FlameNode{Name: "main", Value: 240, Children: []mujica.FlameNode{
{Name: "court.Herald", Value: 150, Children: []mujica.FlameNode{
{Name: "court.Seal", Value: 110, Children: []mujica.FlameNode{{Name: "wax.Melt", Value: 70}, {Name: "ink.Dry", Value: 25}}},
{Name: "ledger.Write", Value: 30},
}},
{Name: "archive.Load", Value: 60, Children: []mujica.FlameNode{{Name: "json.Decode", Value: 45}}},
{Name: "gc", Value: 20},
}}
status := map[string]mujica.DataStatus{"Ready": mujica.DataReady, "Loading": mujica.DataLoading, "Failed": mujica.DataFailed}[st.status]
ui.Column(c).Width(680).Children(func() {
if mujica.Flamegraph(c, root, &st.s, mujica.FlamegraphOptions{Unit: "ms", Status: status, Error: "Profile could not be read", Disabled: st.disabled}).Changed() {
log(fmt.Sprintf("zoom %v focus %v", st.s.Zoom, st.s.Focus))
}
})
}
+230
View File
@@ -0,0 +1,230 @@
package main
// Snippets are the bodies of the Example functions in example_code_test.go.
const codeTerminalCode = `var term mujica.TerminalState
term.Write("\x1b[1m~/court\x1b[0m $ ls\r\n\x1b[34mdecrees\x1b[0m ledger.txt\r\n$ ")
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Terminal(c, &term, mujica.TerminalOptions{
Input: func(data string) { fmt.Printf("to pty: %q\n", data) },
Resized: func(cols, rows int) { fmt.Println("pty size", cols, rows) },
}).Size(380, 200)
}
ui.Render(view, 400, 300, 1)`
const codeTerminalTabsCode = `tabs := []mujica.TerminalTab{{ID: "a", Title: "zsh"}, {ID: "b", Title: "build", Busy: true}}
active := "a"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.TerminalTabs(c, &tabs, &active, mujica.TerminalTabsOptions{}, func(id string) {
ui.Text(c, "terminal "+id)
})
if r.Closed != "" {
fmt.Println("close pty", r.Closed)
}
}
ui.Render(view, 400, 300, 1)`
const codeTerminalSearchCode = `var term mujica.TerminalState
term.Write("build ok\r\nerror: missing seal\r\n")
var search mujica.TerminalSearchState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
ui.Column(c).Gap(8).Children(func() {
mujica.TerminalSearch(c, &term, &search, mujica.TerminalSearchOptions{})
mujica.Terminal(c, &term, mujica.TerminalOptions{}).Height(160)
})
}
ui.Render(view, 400, 300, 1)`
const codeCommandHistoryCode = `history := []mujica.HistoryEntry{{Command: "git status"}, {Command: "go test ./...", ExitCode: 1}}
var state mujica.CommandHistoryState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.CommandHistory(c, history, &state, mujica.CommandHistoryOptions{})
r.Element.Height(240)
if r.Replayed != nil {
fmt.Println("run", r.Replayed.Command)
}
}
ui.Render(view, 400, 300, 1)`
const codeLogViewerCode = `lines := []mujica.LogLine{{Level: mujica.LogInfo, Text: "court opened"}, {Level: mujica.LogError, Text: "seal missing"}}
var state mujica.LogViewerState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.LogViewer(c, lines, &state, mujica.LogViewerOptions{}).Element.Height(240)
}
ui.Render(view, 400, 300, 1)`
const codeProcessListCode = `procs := []mujica.Process{{PID: 7, Name: "archivist", CPU: 42, Memory: 50 << 20}, {PID: 9, Name: "herald", CPU: 3, Memory: 8 << 20}}
var state mujica.ProcessListState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.ProcessList(c, procs, &state, mujica.ProcessListOptions{})
r.Element.Height(240)
if r.End != nil {
fmt.Println("kill", r.End.PID)
}
}
ui.Render(view, 400, 300, 1)`
const codeCodeViewerCode = `lines := []string{"func Seal(d string) int {", "\treturn len(d)", "}"}
var state mujica.CodeViewerState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.CodeViewer(c, lines, &state, mujica.CodeViewerOptions{Language: "go"}).Element.Height(240)
}
ui.Render(view, 400, 300, 1)`
const codeMinimapCode = `lines := []string{"package court", "", "func Seal() {}", "func Herald() {}"}
view := mujica.MinimapView{Top: 0, Visible: 2}
draw := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Minimap(c, lines, &view, mujica.MinimapOptions{Language: "go"}).Changed() {
fmt.Println("scroll editor to", view.Top)
}
}
ui.Render(draw, 400, 300, 1)`
const codeFindWidgetCode = `text := "the seal and the seal"
state := mujica.FindState{Query: "seal", Replacement: "signet", ShowReplace: true}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.FindWidget(c, &text, &state, mujica.FindWidgetOptions{}).Replaced > 0 {
fmt.Println(text)
}
}
ui.Render(view, 400, 300, 1)`
const codeSearchPanelCode = `results := []mujica.SearchFileResult{{Path: "court/seal.go", Matches: []mujica.SearchMatch{{Line: 12, Text: "func Seal() {", Start: 5, End: 9}}}}
state := mujica.SearchPanelState{Query: "seal"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.SearchPanel(c, results, &state, mujica.SearchPanelOptions{})
r.Element.Height(280)
if r.Opened != nil {
fmt.Println("open", r.Opened.Path, r.Opened.Line)
}
}
ui.Render(view, 400, 300, 1)`
const codeProblemsPanelCode = `problems := []mujica.Problem{{Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree"}}
var state mujica.ProblemsPanelState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.ProblemsPanel(c, problems, &state, mujica.ProblemsPanelOptions{})
r.Element.Height(240)
if r.Opened != nil {
fmt.Println("go to", r.Opened.File, r.Opened.Line)
}
}
ui.Render(view, 400, 300, 1)`
const codeOutputPanelCode = `channels := []mujica.OutputChannel{{Name: "Build", Lines: []string{"compiling court", "\x1b[32mok\x1b[0m"}}}
var state mujica.OutputPanelState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.OutputPanel(c, channels, &state, mujica.OutputPanelOptions{})
r.Element.Height(240)
if r.Cleared != "" {
channels[state.Channel].Lines = nil
}
}
ui.Render(view, 400, 300, 1)`
const codeSymbolOutlineCode = `symbols := []mujica.Symbol{{Name: "Court", Kind: mujica.SymbolType, Line: 3, EndLine: 40, Children: []mujica.Symbol{
{Name: "Seal", Kind: mujica.SymbolMethod, Line: 10, EndLine: 20},
}}}
var state mujica.SymbolOutlineState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.SymbolOutline(c, symbols, &state, mujica.SymbolOutlineOptions{CursorLine: 12})
r.Element.Height(240)
if r.Jumped != nil {
fmt.Println("go to line", r.Jumped.Line)
}
}
ui.Render(view, 400, 300, 1)`
const codeCompletionMenuCode = `text := "se"
open := true
items := []mujica.Completion{{Label: "Seal", Kind: mujica.SymbolMethod, Detail: "func()"}, {Label: "Search", Kind: mujica.SymbolFunction}}
var state mujica.CompletionMenuState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
in := mujica.TextInput(c, &text, mujica.TextInputOptions{Label: "Code"})
if r := mujica.CompletionMenu(c, in.Input, &open, items, &state, mujica.CompletionMenuOptions{}); r.Accepted != nil {
text = r.Accepted.Label
}
}
ui.Render(view, 400, 300, 1)`
const codeDebugToolbarCode = `state := mujica.DebugPaused
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
switch mujica.DebugToolbar(c, state, mujica.DebugToolbarOptions{}).Action {
case mujica.DebugContinue:
state = mujica.DebugRunning
case mujica.DebugStop:
state = mujica.DebugStopped
}
}
ui.Render(view, 400, 300, 1)`
const codeBreakpointListCode = `bps := []mujica.Breakpoint{{ID: "a", File: "court/seal.go", Line: 12, Enabled: true}, {ID: "b", File: "main.go", Line: 9, Condition: "n > 3"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.BreakpointList(c, &bps, mujica.BreakpointListOptions{})
r.Element.Height(200)
if r.Toggled != "" || r.Edited != "" {
fmt.Println("sync breakpoints with the debugger")
}
}
ui.Render(view, 400, 300, 1)`
const codeCallStackCode = `threads := []mujica.DebugThread{{ID: "t1", Name: "main", Frames: []mujica.StackFrame{
{ID: "f1", Name: "court.Seal", File: "court/seal.go", Line: 14},
{ID: "f2", Name: "main.main", File: "main.go", Line: 9},
}}}
state := mujica.CallStackState{Current: "f1"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.CallStack(c, threads, &state, mujica.CallStackOptions{})
r.Element.Height(200)
if r.Changed() {
fmt.Println("show frame", state.Current)
}
}
ui.Render(view, 400, 300, 1)`
const codeVariablesPanelCode = `vars := []mujica.Variable{{Name: "decree", Value: "{…}", Children: []mujica.Variable{{Name: "id", Value: "42", Changed: true}}}, {Name: "count", Value: "7"}}
var state mujica.VariablesPanelState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.VariablesPanel(c, vars, &state, mujica.VariablesPanelOptions{})
r.Element.Height(200)
if r.Edited != nil {
fmt.Println("set", r.Edited.Path, "=", r.Edited.Value)
}
}
ui.Render(view, 400, 300, 1)`
const codeHexViewerCode = `data := []byte("MUJICA court ledger\x00\x01")
var state mujica.HexViewerState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.HexViewer(c, data, &state, mujica.HexViewerOptions{}).Element.Height(160)
}
ui.Render(view, 400, 300, 1)`
const codeFlamegraphCode = `root := &mujica.FlameNode{Name: "main", Value: 100, Children: []mujica.FlameNode{
{Name: "court.Seal", Value: 60}, {Name: "ledger.Write", Value: 30},
}}
var state mujica.FlamegraphState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Flamegraph(c, root, &state, mujica.FlamegraphOptions{Unit: "ms"})
}
ui.Render(view, 400, 300, 1)`
+264
View File
@@ -0,0 +1,264 @@
package main
import (
"fmt"
"strings"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func codeProcessListDemo(c *ui.Context, log func(string)) {
type state struct {
procs []mujica.Process
s mujica.ProcessListState
disabled bool
}
s := ui.Local(c.Root(), "326", func() *state {
return &state{procs: []mujica.Process{
{PID: 101, Name: "chancellor", CPU: 3.5, Memory: 200 << 20}, {PID: 7, Name: "archivist", CPU: 42.1, Memory: 50 << 20},
{PID: 330, Name: "herald", CPU: 0.2, Memory: 2 << 30}, {PID: 412, Name: "scribe", CPU: 12.8, Memory: 96 << 20},
}}
})
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
r := mujica.ProcessList(c, st.procs, &st.s, mujica.ProcessListOptions{Disabled: st.disabled})
r.Element.Size(560, 260)
if r.End != nil {
log(fmt.Sprintf("end %s (PID %d)", r.End.Name, r.End.PID))
var kept []mujica.Process
for _, p := range st.procs {
if p.PID != r.End.PID {
kept = append(kept, p)
}
}
st.procs = kept
}
}
var codeDemoSource = strings.Split(`package court
import "errors"
// Decree is an order of the court.
type Decree struct {
ID int
Title string
}
/* Seal stamps a decree
with the royal wax. */
func Seal(d Decree) (string, error) {
if d.Title == "" {
return "", errors.New("untitled decree")
}
for i := 0; i < 3; i++ {
d.ID += i
}
return "sealed: " + d.Title, nil
}
func Herald(ds []Decree) int {
n := 0
for _, d := range ds {
if _, err := Seal(d); err == nil {
n++
}
}
return n
}`, "\n")
func codeCodeViewerDemo(c *ui.Context, log func(string)) {
type state struct {
s mujica.CodeViewerState
lang string
disabled bool
}
s := ui.Local(c.Root(), "327", func() *state { return &state{lang: "go"} })
st := *s
codeDemoControls(c, func() {
codeDemoCheck(c, &st.disabled, "Disabled", log)
langs := []mujica.SelectOption[string]{{Value: "go", Label: "Go"}, {Value: "", Label: "Plain"}}
if mujica.Select(c, &st.lang, langs, mujica.SelectOptions{Label: "Language"}).Changed() {
log("language " + st.lang)
}
if mujica.Button(c, "Go to line 20", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
st.s.GoTo(19)
log("go to 20")
}
})
r := mujica.CodeViewer(c, codeDemoSource, &st.s, mujica.CodeViewerOptions{Language: st.lang, Label: "Source", Disabled: st.disabled})
r.Element.Size(600, 360)
if r.Changed() {
log(fmt.Sprintf("line %d", st.s.Line()+1))
}
}
func codeMinimapDemo(c *ui.Context, log func(string)) {
type state struct {
s mujica.CodeViewerState
view mujica.MinimapView
lines []string
disabled bool
}
s := ui.Local(c.Root(), "328", func() *state {
var lines []string
for range 12 {
lines = append(lines, codeDemoSource...)
}
return &state{lines: lines}
})
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
first, last := st.s.Visible()
st.view = mujica.MinimapView{Top: first, Visible: max(last-first+1, 1)}
ui.Row(c).Gap(8).Height(360).AlignItems(ui.Stretch).Children(func() {
mujica.CodeViewer(c, st.lines, &st.s, mujica.CodeViewerOptions{Language: "go"}).Element.Width(520)
if mujica.Minimap(c, st.lines, &st.view, mujica.MinimapOptions{Language: "go", Disabled: st.disabled}).Changed() {
st.s.ScrollTo(st.view.Top)
log(fmt.Sprintf("jump to line %d", st.view.Top+1))
}
})
}
func codeFindWidgetDemo(c *ui.Context, log func(string)) {
type state struct {
text string
s mujica.FindState
readOnly, disabled bool
}
s := ui.Local(c.Root(), "329", func() *state {
return &state{text: "The seal of the court; seals are kept sealed. Seal 42 and seal 7.", s: mujica.FindState{Query: "seal"}}
})
st := *s
codeDemoControls(c, func() {
codeDemoCheck(c, &st.readOnly, "Read-only", log)
codeDemoCheck(c, &st.disabled, "Disabled", log)
})
ui.Column(c).Width(620).Gap(12).Children(func() {
r := mujica.FindWidget(c, &st.text, &st.s, mujica.FindWidgetOptions{ReadOnly: st.readOnly, Disabled: st.disabled})
if r.Moved {
log(fmt.Sprintf("%d matches, at %d", len(st.s.Matches()), st.s.Current+1))
}
if r.Replaced > 0 {
log(fmt.Sprintf("replaced %d", r.Replaced))
}
var spans []ui.Span
at := 0
for i, m := range st.s.Matches() {
spans = append(spans, ui.Span{Text: st.text[at:m[0]]})
sp := ui.Span{Text: st.text[m[0]:m[1]], Background: mujica.Tokens(c).Selection, Underline: true}
if i == st.s.Current {
sp.Weight = 700
}
spans = append(spans, sp)
at = m[1]
}
spans = append(spans, ui.Span{Text: st.text[at:]})
ui.RichText(c, spans...)
})
}
func codeSearchPanelDemo(c *ui.Context, log func(string)) {
type state struct {
s mujica.SearchPanelState
results []mujica.SearchFileResult
disabled bool
}
s := ui.Local(c.Root(), "330", func() *state { return &state{s: mujica.SearchPanelState{Query: "seal"}} })
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
// The demo searches its own two files.
files := map[string][]string{"court/seal.go": codeDemoSource, "docs/ledger.md": {"# Ledger", "The seal is kept by the chancellor.", "Seals expire after a year."}}
if st.results == nil || st.s.Query != "" && len(st.results) == 0 {
st.results = codeDemoSearch(files, st.s.Query, st.s.Include, st.s.Exclude)
}
r := mujica.SearchPanel(c, st.results, &st.s, mujica.SearchPanelOptions{Disabled: st.disabled})
r.Element.Size(560, 380)
if r.Changed {
st.results = codeDemoSearch(files, st.s.Query, st.s.Include, st.s.Exclude)
log(fmt.Sprintf("search %q include %q exclude %q", st.s.Query, st.s.Include, st.s.Exclude))
}
if r.Opened != nil {
log(fmt.Sprintf("open %s:%d", r.Opened.Path, r.Opened.Line))
}
}
// codeDemoSearch finds q in the demo files.
func codeDemoSearch(files map[string][]string, q, include, exclude string) []mujica.SearchFileResult {
out := []mujica.SearchFileResult{}
if q == "" {
return out
}
for _, path := range []string{"court/seal.go", "docs/ledger.md"} {
if include != "" && !strings.Contains(path, strings.Trim(include, "*")) || exclude != "" && strings.Contains(path, strings.Trim(exclude, "*")) {
continue
}
f := mujica.SearchFileResult{Path: path}
for i, line := range files[path] {
if at := strings.Index(strings.ToLower(line), strings.ToLower(q)); at >= 0 {
f.Matches = append(f.Matches, mujica.SearchMatch{Line: i + 1, Text: line, Start: at, End: at + len(q)})
}
}
if len(f.Matches) > 0 {
out = append(out, f)
}
}
return out
}
func codeProblemsDemo(c *ui.Context, log func(string)) {
type state struct {
s mujica.ProblemsPanelState
disabled bool
}
s := ui.Local(c.Root(), "331", func() *state { return &state{} })
st := *s
codeDemoControls(c, func() { codeDemoCheck(c, &st.disabled, "Disabled", log) })
problems := []mujica.Problem{
{Severity: mujica.ProblemError, File: "court/seal.go", Line: 12, Col: 5, Message: "undefined: Decree", Source: "compiler"},
{Severity: mujica.ProblemWarning, File: "court/seal.go", Line: 30, Col: 2, Message: "unused variable wax", Source: "vet"},
{Severity: mujica.ProblemInfo, File: "docs/ledger.md", Line: 4, Col: 1, Message: "line is longer than 80", Source: "lint"},
{Severity: mujica.ProblemError, File: "main.go", Line: 9, Col: 14, Message: "missing return", Source: "compiler"},
}
r := mujica.ProblemsPanel(c, problems, &st.s, mujica.ProblemsPanelOptions{Disabled: st.disabled})
r.Element.Size(620, 320)
if r.Opened != nil {
log(fmt.Sprintf("go to %s:%d:%d", r.Opened.File, r.Opened.Line, r.Opened.Col))
}
}
func codeOutputDemo(c *ui.Context, log func(string)) {
type state struct {
channels []mujica.OutputChannel
s mujica.OutputPanelState
disabled bool
}
s := ui.Local(c.Root(), "332", func() *state {
var build []string
for i := range 300 {
build = append(build, fmt.Sprintf("compiling unit %d of 300", i+1))
}
return &state{channels: []mujica.OutputChannel{{Name: "Build", Lines: build},
{Name: "Tests", Lines: []string{"\x1b[32mPASS\x1b[0m court/seal", "\x1b[31mFAIL\x1b[0m court/ledger: expected 3 seals", "\x1b[1mok\x1b[0m 2 of 3 packages"}}}}
})
st := *s
codeDemoControls(c, func() {
codeDemoCheck(c, &st.disabled, "Disabled", log)
if mujica.Button(c, "Write line", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
ch := &st.channels[st.s.Channel]
ch.Lines = append(ch.Lines, fmt.Sprintf("line %d", len(ch.Lines)+1))
log("write to " + ch.Name)
}
})
r := mujica.OutputPanel(c, st.channels, &st.s, mujica.OutputPanelOptions{Disabled: st.disabled})
r.Element.Size(560, 300)
if r.Changed {
log("channel " + st.channels[st.s.Channel].Name)
}
if r.Cleared != "" {
st.channels[st.s.Channel].Lines = nil
log("cleared " + r.Cleared)
}
}
+296 -1
View File
@@ -1,3 +1,298 @@
package main
var gitPages = []page{}
var gitPages = []page{
{
ID: "301", Name: "DiffViewer", ZhName: "差异查看器 比较 diff", Category: "Git",
Demo: gitDiffViewerDemo,
Code: `var state mujica.DiffViewerState
from := "func main() {\n\tprintln(\"hi\")\n}\n"
to := "func main() {\n\tprintln(\"hello\")\n}\n"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DiffViewer(c, &state, from, to, mujica.DiffViewerOptions{Language: "go", SideBySide: true}).Height(240)
}
ui.Render(view, 400, 300, 1)`,
Keys: "Tab focuses the list; arrows and Page Up/Down scroll. Fold buttons take Tab; Enter or Space shows the hidden lines.",
},
{
ID: "302", Name: "InlineDiff", ZhName: "行内差异 词级差异", Category: "Git",
Demo: gitInlineDiffDemo,
Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.InlineDiff(c, "The court meets at noon", "The court meets at dusk", mujica.InlineDiffOptions{})
}
ui.Render(view, 400, 300, 1)`,
Keys: "Not focusable; each line is read with its full text.",
},
{
ID: "303", Name: "ThreeWayMerge", ZhName: "三方合并 合并冲突", Category: "Git",
Demo: gitThreeWayMergeDemo,
Code: `var state mujica.ThreeWayMergeState
base, local, remote := "a\nb\nc", "a\nB1\nc", "a\nB2\nc"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.ThreeWayMerge(c, &state, base, local, remote, mujica.ThreeWayMergeOptions{})
r.Element.Height(280)
if done, total := state.Resolved(); r.Changed() && done == total {
_ = state.Result() // write the merged file
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Tab reaches the take buttons of each conflict; Enter or Space chooses. The list scrolls with the arrows.",
},
{
ID: "304", Name: "ConflictResolver", ZhName: "冲突解决 冲突标记", Category: "Git",
Demo: gitConflictResolverDemo,
Code: `var state mujica.ConflictResolverState
text := "a\n<<<<<<< HEAD\nmine\n=======\ntheirs\n>>>>>>> feature\nb"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ConflictResolver(c, &state, text, mujica.ConflictResolverOptions{}).Changed() {
_ = state.Result() // save the file once every block is resolved
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Tab moves through each block's buttons; Enter or Space accepts a side; Undo clears the choice.",
},
{
ID: "305", Name: "ChangesList", ZhName: "更改列表 暂存 未暂存", Category: "Git",
Demo: gitChangesListDemo,
Code: `var state mujica.ChangesListState
files := []mujica.ChangedFile{
{Path: "main.go", Status: mujica.GitModified, Staged: true},
{Path: "notes.txt", Status: mujica.GitUntracked},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.ChangesList(c, &state, files, mujica.ChangesListOptions{})
r.Element.Height(240)
if action, path, ok := r.Action(); ok {
_, _ = action, path // run git add or git restore --staged
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Up/Down choose a file, skipping group titles; typing jumps by name; stage buttons take Tab, Enter or Space.",
},
{
ID: "306", Name: "CommitInput", ZhName: "提交输入 提交信息", Category: "Git",
Demo: gitCommitInputDemo,
Code: `msg := mujica.CommitMessage{}
busy := false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.CommitInput(c, &msg, mujica.CommitInputOptions{Busy: busy})
if r.Committed() || r.Amended() {
busy = true // run git commit, then clear busy
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Enter in the summary or Cmd/Ctrl+Enter in the description commits; Tab reaches Amend and Commit.",
},
{
ID: "307", Name: "CommitGraph", ZhName: "提交图谱 泳道 分支图", Category: "Git",
Demo: gitCommitGraphDemo,
Code: `var state mujica.CommitGraphState
commits := []mujica.Commit{
{Hash: "c3", Parents: []string{"c2", "b1"}, Subject: "Merge branch", Refs: []string{"main"}},
{Hash: "b1", Parents: []string{"c1"}, Subject: "Feature"},
{Hash: "c2", Parents: []string{"c1"}, Subject: "Fix"},
{Hash: "c1", Subject: "Initial commit"},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.CommitGraph(c, &state, commits, mujica.CommitGraphOptions{}).Element.Height(240)
}
ui.Render(view, 400, 300, 1)`,
Keys: "Up/Down/Home/End choose a commit; typing jumps by subject; Enter opens it.",
},
{
ID: "308", Name: "CommitList", ZhName: "提交列表 提交历史", Category: "Git",
Demo: gitCommitListDemo,
Code: `var state mujica.ListState[string]
commits := []mujica.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Add the commit list"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.CommitList(c, &state, commits, mujica.CommitListOptions{})
r.Element.Height(240)
if hash, ok := state.Selected(); ok && r.Submitted() {
_ = hash // show the commit
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Up/Down/Home/End choose a commit; typing jumps by subject; Enter opens it.",
},
{
ID: "309", Name: "BranchSelector", ZhName: "分支选择器 切换分支", Category: "Git",
Demo: gitBranchSelectorDemo,
Code: `branch := "main"
branches := []mujica.Branch{{Name: "main", Current: true}, {Name: "origin/main", Remote: true}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.BranchSelector(c, &branch, branches, mujica.BranchSelectorOptions{AllowCreate: true})
if name, ok := r.Created(); ok {
_ = name // run git switch -c name
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Enter or Space opens; type to filter; Up/Down move; Enter chooses or creates; Escape closes.",
},
{
ID: "310", Name: "BranchList", ZhName: "分支列表 上游 领先落后", Category: "Git",
Demo: gitBranchListDemo,
Code: `var state mujica.ListState[string]
branches := []mujica.Branch{{Name: "main", Current: true, Upstream: "origin/main", Ahead: 2}}
actions := []mujica.GitAction{{ID: "delete", Label: "Delete", Icon: "trash-2"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.BranchList(c, &state, branches, mujica.BranchListOptions{Actions: actions})
r.Element.Height(200)
if action, name, ok := r.Action(); ok {
_, _ = action, name // run the action on the branch
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Up/Down choose a branch; row actions take Tab, Enter or Space.",
},
{
ID: "311", Name: "TagList", ZhName: "标签列表 tag", Category: "Git",
Demo: gitTagListDemo,
Code: `var state mujica.ListState[string]
tags := []mujica.GitTag{{Name: "v1.1.0", Target: "a1b2c3d4", Annotation: "Release"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.TagList(c, &state, tags, mujica.TagListOptions{})
r.Element.Height(200)
if _, hash, ok := r.Action(); ok {
_ = hash // show the tagged commit
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Up/Down choose a tag; the jump button takes Tab, Enter or Space.",
},
{
ID: "312", Name: "StashList", ZhName: "贮藏列表 stash", Category: "Git",
Demo: gitStashListDemo,
Code: `var state mujica.ListState[string]
stashes := []mujica.Stash{{Ref: "stash@{0}", Message: "WIP on main", Branch: "main"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.StashList(c, &state, stashes, mujica.StashListOptions{})
r.Element.Height(200)
if action, ref, ok := r.Action(); ok {
_, _ = action, ref // run git stash apply, pop or drop
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Up/Down choose a stash; Apply, Pop and Drop take Tab, Enter or Space.",
},
{
ID: "313", Name: "BlameView", ZhName: "逐行追溯 blame 作者", Category: "Git",
Demo: gitBlameViewDemo,
Code: `var state mujica.BlameViewState
lines := []mujica.BlameLine{
{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "package main"},
{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Summary: "Add main", Text: "func main() {}"},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.BlameView(c, &state, lines, mujica.BlameViewOptions{Language: "go"}).Height(240)
}
ui.Render(view, 400, 300, 1)`,
Keys: "Tab focuses the code; arrows scroll; typing jumps to a line starting with the text. Hover the left column for details.",
},
{
ID: "314", Name: "FileHistory", ZhName: "文件历史 单文件提交", Category: "Git",
Demo: gitFileHistoryDemo,
Code: `var state mujica.ListState[string]
commits := []mujica.Commit{{Hash: "a1b2c3d4", Author: "Sakiko", When: time.Now(), Subject: "Edit main.go"}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.FileHistory(c, &state, "main.go", commits, mujica.FileHistoryOptions{})
r.Element.Height(240)
if _, hash, ok := r.Action(); ok {
_ = hash // open the diff of main.go at hash
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Up/Down choose a commit; View changes takes Tab, Enter or Space.",
},
{
ID: "315", Name: "PullRequestCard", ZhName: "拉取请求卡片 合并请求 PR", Category: "Git",
Demo: gitPullRequestCardDemo,
Code: `pr := mujica.PullRequest{Number: 42, Title: "Add git components", Author: "Sakiko", Head: "feature", Base: "main",
ChecksPassed: 3, Reviewers: []mujica.Reviewer{{Name: "Uika", State: mujica.ReviewApproved}}, Added: 120, Deleted: 8, Files: 6}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.PullRequestCard(c, pr, mujica.PullRequestCardOptions{}).Changed() {
// open the pull request
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Tab reaches the title link; Enter or Space opens the pull request.",
},
{
ID: "316", Name: "ReviewComment", ZhName: "评审评论 代码评审", Category: "Git",
Demo: gitReviewCommentDemo,
Code: `thread := mujica.ReviewThread{File: "main.go", Line: 12, Messages: []mujica.ReviewMessage{
{Author: "Uika", When: time.Now(), Body: "Can this return early?"},
}}
draft := ""
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.ReviewComment(c, thread, &draft, mujica.ReviewCommentOptions{})
if text, ok := r.Replied(); ok {
thread.Messages = append(thread.Messages, mujica.ReviewMessage{Author: "Sakiko", When: time.Now(), Body: text})
}
if r.Resolved() || r.Reopened() {
thread.Resolved = !thread.Resolved
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Tab moves through the reply field and buttons; Cmd/Ctrl+Enter sends the reply.",
},
{
ID: "317", Name: "DiffStat", ZhName: "差异统计 增删行数", Category: "Git",
Demo: gitDiffStatDemo,
Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DiffStat(c, 120, 8, mujica.DiffStatOptions{})
}
ui.Render(view, 400, 300, 1)`,
Keys: "Not focusable; read as additions and deletions.",
},
{
ID: "318", Name: "GitStatusBadge", ZhName: "Git 状态标记 文件状态", Category: "Git",
Demo: gitStatusBadgeDemo,
Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.GitStatusBadge(c, mujica.GitModified, mujica.GitStatusBadgeOptions{ShowLabel: true})
}
ui.Render(view, 400, 300, 1)`,
Keys: "Not focusable; each badge is read by its status name.",
},
{
ID: "319", Name: "CommandBlock", ZhName: "命令块 终端输出 退出码", Category: "Git",
Demo: gitCommandBlockDemo,
Code: `open := true
run := mujica.CommandRun{Command: "go test ./...", Output: "\x1b[32mok\x1b[0m example 0.4s", Duration: 400 * time.Millisecond}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.CommandBlock(c, run, &open, mujica.CommandBlockOptions{})
if what, ok := r.Copied(); ok {
_ = what // "command" or "output" is on the clipboard
}
}
ui.Render(view, 400, 300, 1)`,
Keys: "Tab reaches the header; Enter or Space folds the output; the copy buttons take Tab.",
},
{
ID: "320", Name: "AnsiText", ZhName: "ANSI 文本 终端颜色", Category: "Git",
Demo: gitAnsiTextDemo,
Code: `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AnsiText(c, "\x1b[1;32mPASS\x1b[0m 12 tests\n\x1b[31mFAIL\x1b[0m 1 test", mujica.AnsiTextOptions{})
}
ui.Render(view, 400, 300, 1)`,
Keys: "Not focusable; shows the text with its styles.",
},
}
+134
View File
@@ -0,0 +1,134 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func gitPullRequestCardDemo(c *ui.Context, log func(string)) {
type state struct {
st int
failed, disabled bool
}
s := ui.Local(c.Root(), "315", func() state { return state{} })
navigationDemo(c, func() {
mujica.SegmentedControl(c, &s.st, []mujica.Segment{{Label: "Open"}, {Label: "Draft"}, {Label: "Merged"}, {Label: "Closed"}}, mujica.SegmentedControlOptions{Label: "State"})
navigationCheck(c, &s.failed, "Failing check", log)
navigationCheck(c, &s.disabled, "Disabled", log)
}, func() {
pr := mujica.PullRequest{Number: 318, Title: "Git components 301–320", Author: "Sakiko", State: mujica.PullRequestState(s.st),
Head: "feature/git", Base: "main", ChecksPassed: 12, ChecksPending: 1,
Reviewers: []mujica.Reviewer{{Name: "Uika", State: mujica.ReviewApproved}, {Name: "Mutsumi", State: mujica.ReviewChangesRequested}, {Name: "Umiri"}},
Added: 2412, Deleted: 310, Files: 46}
if s.failed {
pr.ChecksFailed = 1
}
if mujica.PullRequestCard(c, pr, mujica.PullRequestCardOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("Open #%d", pr.Number))
}
})
}
func gitReviewCommentDemo(c *ui.Context, log func(string)) {
type state struct {
thread mujica.ReviewThread
draft string
disabled bool
}
s := ui.Local(c.Root(), "316", func() state {
return state{thread: mujica.ReviewThread{File: "commit_graph.go", Line: 88, Messages: []mujica.ReviewMessage{
{Author: "Uika", When: gitDemoTime, Body: "Should merge lanes reuse the parent's color?"},
{Author: "Sakiko", When: gitDemoTime.Add(time.Hour), Body: "Yes, the first parent keeps it."},
}}}
})
navigationDemo(c, func() {
navigationCheck(c, &s.disabled, "Disabled", log)
}, func() {
r := mujica.ReviewComment(c, s.thread, &s.draft, mujica.ReviewCommentOptions{Disabled: s.disabled})
if text, ok := r.Replied(); ok {
log("Reply: " + text)
s.thread.Messages = append(s.thread.Messages, mujica.ReviewMessage{Author: "You", When: c.Now(), Body: text})
}
if r.Resolved() {
log("Resolved")
s.thread.Resolved = true
}
if r.Reopened() {
log("Reopened")
s.thread.Resolved = false
}
})
}
func gitDiffStatDemo(c *ui.Context, log func(string)) {
type state struct{ added, deleted, blocks float64 }
s := ui.Local(c.Root(), "317", func() state { return state{added: 120, deleted: 30, blocks: 5} })
navigationDemo(c, func() {
navigationSlider(c, &s.added, "Added", 0, 500, log)
navigationSlider(c, &s.deleted, "Deleted", 0, 500, log)
navigationSlider(c, &s.blocks, "Blocks", 1, 10, log)
}, func() {
mujica.DiffStat(c, int(s.added), int(s.deleted), mujica.DiffStatOptions{Blocks: int(s.blocks)})
})
}
func gitStatusBadgeDemo(c *ui.Context, log func(string)) {
type state struct{ labels bool }
s := ui.Local(c.Root(), "318", func() state { return state{} })
navigationDemo(c, func() {
navigationCheck(c, &s.labels, "Show labels", log)
}, func() {
ui.Row(c).Gap(16).Wrap().Children(func() {
for st := mujica.GitModified; st <= mujica.GitCopied; st++ {
mujica.GitStatusBadge(c, st, mujica.GitStatusBadgeOptions{ShowLabel: s.labels})
}
})
})
}
func gitCommandBlockDemo(c *ui.Context, log func(string)) {
type state struct {
open, failed, running bool
}
s := ui.Local(c.Root(), "319", func() state { return state{open: true} })
navigationDemo(c, func() {
navigationCheck(c, &s.failed, "Failed", log)
navigationCheck(c, &s.running, "Running", log)
}, func() {
run := mujica.CommandRun{Command: "go test -run Git ./...", Duration: 3214 * time.Millisecond,
Output: "\x1b[32mok\x1b[0m \tmujicaui\t3.214s\n\x1b[33mwarn\x1b[0m slow test: TestCommitGraphVirtualized"}
if s.failed {
run.ExitCode = 1
run.Output = "\x1b[31m--- FAIL\x1b[0m: TestDiffViewer (0.02s)\n\x1b[1mFAIL\x1b[0m\tmujicaui\t0.401s"
}
run.Running = s.running
r := mujica.CommandBlock(c, run, &s.open, mujica.CommandBlockOptions{})
if r.Toggled() {
log(fmt.Sprintf("Toggled: open=%v", s.open))
}
if what, ok := r.Copied(); ok {
log("Copied " + what)
}
})
}
func gitAnsiTextDemo(c *ui.Context, log func(string)) {
type state struct{ wrap bool }
s := ui.Local(c.Root(), "320", func() state { return state{} })
navigationDemo(c, func() {
navigationCheck(c, &s.wrap, "Wrap", log)
}, func() {
ui.Column(c).Padding(12).Background(mujica.Tokens(c).Surface).Radius(4).Children(func() {
mujica.AnsiText(c, "\x1b[1mBuilding\x1b[0m mujica \x1b[2m(v1.1)\x1b[0m\n"+
"\x1b[32m✓\x1b[0m 300 passed \x1b[31m✗\x1b[0m 2 failed \x1b[33m!\x1b[0m 1 skipped\n"+
"\x1b[30m0\x1b[31m1\x1b[32m2\x1b[33m3\x1b[34m4\x1b[35m5\x1b[36m6\x1b[37m7\x1b[90m8\x1b[91m9\x1b[92mA\x1b[93mB\x1b[94mC\x1b[95mD\x1b[96mE\x1b[97mF\x1b[0m\n"+
"\x1b[38;5;208m256-color\x1b[0m \x1b[38;2;125;32;52mtrue color\x1b[0m \x1b[4munderline\x1b[0m \x1b[3mitalic\x1b[0m \x1b[9mstruck\x1b[0m \x1b[7m inverse \x1b[0m\n"+
"A long line that only breaks when wrapping is on, so the terminal layout of columns stays intact as it was printed.\n"+
"Unknown escapes stay as text: \x1b[2K(cleared line)", mujica.AnsiTextOptions{Wrap: s.wrap})
})
})
}
+367
View File
@@ -0,0 +1,367 @@
package main
import (
"fmt"
"strings"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
var gitDemoTime = time.Date(2026, 10, 5, 9, 30, 0, 0, time.UTC)
const gitDemoOld = "package court\n\nimport \"fmt\"\n\n// Greet welcomes a guest.\nfunc Greet(name string) {\n\tfmt.Println(\"Welcome,\", name)\n}\n\nfunc Leave() {}\n\nfunc Dance() {}\n\nfunc Bow() {}\n"
const gitDemoNew = "package court\n\nimport \"fmt\"\n\n// Greet welcomes a guest by title.\nfunc Greet(title, name string) {\n\tfmt.Println(\"Welcome,\", title, name)\n}\n\nfunc Leave() {}\n\nfunc Dance() {}\n\nfunc Bow() {}\n\nfunc Curtsy() {}\n"
func gitDemoCommits(n int) []mujica.Commit {
out := make([]mujica.Commit, n)
authors := []string{"Sakiko", "Mutsumi", "Uika", "Umiri", "Nyamu"}
for i := range out {
out[i] = mujica.Commit{Hash: fmt.Sprintf("%016x%024x", uint64(n-i)*0x9E3779B97F4A7C15, n-i), Author: authors[i%5],
When: gitDemoTime.Add(-time.Duration(i) * 37 * time.Minute), Subject: fmt.Sprintf("Chapter %d of the chronicle", n-i)}
if i+1 < n {
out[i].Parents = []string{fmt.Sprintf("%016x%024x", uint64(n-i-1)*0x9E3779B97F4A7C15, n-i-1)}
}
}
if n > 0 {
out[0].Refs = []string{"main", "origin/main"}
}
return out
}
func gitDemoGraph() []mujica.Commit {
cs := []mujica.Commit{
{Hash: "e7", Parents: []string{"e5", "f3"}, Subject: "Merge branch 'feature/lanes'", Refs: []string{"main"}},
{Hash: "f3", Parents: []string{"f2"}, Subject: "Color lanes by branch"},
{Hash: "e5", Parents: []string{"e4", "h1"}, Subject: "Merge hotfix"},
{Hash: "h1", Parents: []string{"e3"}, Subject: "Fix the overture timing", Refs: []string{"hotfix"}},
{Hash: "f2", Parents: []string{"f1"}, Subject: "Draw merge lines"},
{Hash: "e4", Parents: []string{"e3"}, Subject: "Tune the ballroom palette"},
{Hash: "f1", Parents: []string{"e3"}, Subject: "Start lane layout"},
{Hash: "e3", Parents: []string{"e2"}, Subject: "Add the commit list"},
{Hash: "e2", Parents: []string{"e1"}, Subject: "Add the blame view"},
{Hash: "e1", Subject: "Initial commit", Refs: []string{"v1.0.0"}},
}
for i := range cs {
cs[i].Author = []string{"Sakiko", "Mutsumi", "Uika"}[i%3]
cs[i].When = gitDemoTime.Add(-time.Duration(i) * 2 * time.Hour)
}
return cs
}
func gitDemoBranches() []mujica.Branch {
return []mujica.Branch{
{Name: "main", Current: true, Upstream: "origin/main", Ahead: 2, Behind: 1},
{Name: "feature/lanes", Upstream: "origin/feature/lanes", Ahead: 5},
{Name: "fix/blame"},
{Name: "origin/main", Remote: true},
{Name: "origin/feature/lanes", Remote: true},
{Name: "origin/release/1.1", Remote: true},
}
}
func gitDiffViewerDemo(c *ui.Context, log func(string)) {
type state struct {
viewer mujica.DiffViewerState
split, plain bool
context float64
}
s := ui.Local(c.Root(), "301", func() state { return state{context: 2} })
navigationDemo(c, func() {
navigationCheck(c, &s.split, "Side by side", log)
navigationCheck(c, &s.plain, "Plain text", log)
navigationSlider(c, &s.context, "Context", 1, 6, log)
}, func() {
lang := "go"
if s.plain {
lang = ""
}
mujica.DiffViewer(c, &s.viewer, gitDemoOld, gitDemoNew, mujica.DiffViewerOptions{SideBySide: s.split, Context: int(s.context), Language: lang}).Height(380)
})
}
func gitInlineDiffDemo(c *ui.Context, log func(string)) {
type state struct {
from, to string
mono bool
}
s := ui.Local(c.Root(), "302", func() state {
return state{from: "The court meets at noon in the east hall.", to: "The council meets at dusk in the east hall."}
})
navigationDemo(c, func() {
navigationCheck(c, &s.mono, "Monospace", log)
}, func() {
if mujica.TextInput(c, &s.from, mujica.TextInputOptions{Label: "Old line"}).Changed() {
log("Old: " + s.from)
}
if mujica.TextInput(c, &s.to, mujica.TextInputOptions{Label: "New line"}).Changed() {
log("New: " + s.to)
}
mujica.InlineDiff(c, s.from, s.to, mujica.InlineDiffOptions{Mono: s.mono})
})
}
func gitThreeWayMergeDemo(c *ui.Context, log func(string)) {
type state struct {
merge mujica.ThreeWayMergeState
disabled bool
}
s := ui.Local(c.Root(), "303", func() state { return state{} })
base := "title: Overture\ntempo: 96\nkey: D minor\nlength: 4:10\nhall: east"
ours := "title: Overture\ntempo: 104\nkey: D minor\nlength: 4:10\nhall: east\nencore: yes"
theirs := "title: Overture\ntempo: 92\nkey: D minor\nlength: 4:30\nhall: east"
navigationDemo(c, func() {
navigationCheck(c, &s.disabled, "Disabled", log)
}, func() {
r := mujica.ThreeWayMerge(c, &s.merge, base, ours, theirs, mujica.ThreeWayMergeOptions{Disabled: s.disabled})
r.Element.Height(360)
if r.Changed() {
done, total := s.merge.Resolved()
log(fmt.Sprintf("Changed: %d of %d resolved", done, total))
}
ui.Text(c, s.merge.Result()).Font("monospace").TextColor(mujica.Tokens(c).TextMuted)
})
}
func gitConflictResolverDemo(c *ui.Context, log func(string)) {
type state struct {
res mujica.ConflictResolverState
disabled bool
}
s := ui.Local(c.Root(), "304", func() state { return state{} })
text := "# Program\n<<<<<<< HEAD\nOpening: Overture in D minor\n=======\nOpening: Prelude in G major\n>>>>>>> feature/program\nInterval: 15 minutes\n<<<<<<< HEAD\nFinale: Masquerade\n=======\nFinale: Ave Mujica\n>>>>>>> feature/program"
navigationDemo(c, func() {
navigationCheck(c, &s.disabled, "Disabled", log)
}, func() {
if mujica.ConflictResolver(c, &s.res, text, mujica.ConflictResolverOptions{Disabled: s.disabled}).Changed() {
done, total := s.res.Resolved()
log(fmt.Sprintf("Changed: %d of %d resolved", done, total))
}
})
}
func gitChangesListDemo(c *ui.Context, log func(string)) {
type state struct {
list mujica.ChangesListState
files []mujica.ChangedFile
disabled bool
}
s := ui.Local(c.Root(), "305", func() state {
return state{files: []mujica.ChangedFile{
{Path: "commit_graph.go", Status: mujica.GitAdded, Staged: true},
{Path: "internal/git.go", Status: mujica.GitModified},
{Path: "docs/old.md", Status: mujica.GitDeleted},
{Path: "notes.txt", Status: mujica.GitUntracked},
{Path: "icons/svg/tag.svg", Status: mujica.GitRenamed},
}}
})
navigationDemo(c, func() {
navigationCheck(c, &s.disabled, "Disabled", log)
}, func() {
r := mujica.ChangesList(c, &s.list, s.files, mujica.ChangesListOptions{Disabled: s.disabled})
r.Element.Height(320)
if a, path, ok := r.Action(); ok {
log("Action: " + a + " " + path)
s.files = gitStage(s.files, a, path)
}
if r.Changed() && s.list.Selected() >= 0 {
log("Selected: " + s.files[s.list.Selected()].Path)
}
})
}
// gitStage applies a stage action to a copy of files.
func gitStage(files []mujica.ChangedFile, action, path string) []mujica.ChangedFile {
out := append([]mujica.ChangedFile(nil), files...)
for i := range out {
switch {
case action == "stage_all":
out[i].Staged = true
case action == "unstage_all":
out[i].Staged = false
case out[i].Path == path:
out[i].Staged = action == "stage"
}
}
return out
}
func gitCommitInputDemo(c *ui.Context, log func(string)) {
type state struct {
msg mujica.CommitMessage
busy, disabled bool
until time.Time
}
s := ui.Local(c.Root(), "306", func() state { return state{} })
if s.busy && c.Now().After(s.until) {
s.busy = false
s.msg = mujica.CommitMessage{}
log("Committed")
}
if s.busy {
c.After(s.until.Sub(c.Now()))
}
navigationDemo(c, func() {
navigationCheck(c, &s.disabled, "Disabled", log)
}, func() {
r := mujica.CommitInput(c, &s.msg, mujica.CommitInputOptions{Busy: s.busy, Disabled: s.disabled})
if r.Committed() || r.Amended() {
log(fmt.Sprintf("Submit: amend=%v title=%q", r.Amended(), s.msg.Title))
s.busy, s.until = true, c.Now().Add(1200*time.Millisecond)
}
})
}
func gitCommitGraphDemo(c *ui.Context, log func(string)) {
type state struct {
graph mujica.CommitGraphState
big bool
}
s := ui.Local(c.Root(), "307", func() state { return state{} })
small, big := gitDemoGraph(), gitDemoCommits(100000)
navigationDemo(c, func() {
navigationCheck(c, &s.big, "100,000 commits", log)
}, func() {
commits := small
if s.big {
commits = big
}
r := mujica.CommitGraph(c, &s.graph, commits, mujica.CommitGraphOptions{})
r.Element.Height(380)
if hash, ok := s.graph.List.Selected(); ok && (r.Changed() || r.Submitted()) {
log(fmt.Sprintf("Selected %s, open=%v", hash[:min(7, len(hash))], r.Submitted()))
}
})
}
func gitCommitListDemo(c *ui.Context, log func(string)) {
type state struct {
list mujica.ListState[string]
loading bool
}
s := ui.Local(c.Root(), "308", func() state { return state{} })
commits := gitDemoCommits(40)
navigationDemo(c, func() {
navigationCheck(c, &s.loading, "Loading more", log)
}, func() {
st := mujica.DataReady
if s.loading {
st = mujica.DataLoadingMore
}
r := mujica.CommitList(c, &s.list, commits, mujica.CommitListOptions{Status: st})
r.Element.Height(360)
if hash, ok := s.list.Selected(); ok && (r.Changed() || r.Submitted()) {
log(fmt.Sprintf("Selected %s, open=%v", hash[:7], r.Submitted()))
}
})
}
func gitBranchSelectorDemo(c *ui.Context, log func(string)) {
type state struct {
branch string
create, disabled bool
branches []mujica.Branch
}
s := ui.Local(c.Root(), "309", func() state { return state{branch: "main", create: true, branches: gitDemoBranches()} })
navigationDemo(c, func() {
navigationCheck(c, &s.create, "Allow create", log)
navigationCheck(c, &s.disabled, "Disabled", log)
}, func() {
r := mujica.BranchSelector(c, &s.branch, s.branches, mujica.BranchSelectorOptions{AllowCreate: s.create, Disabled: s.disabled})
if r.Changed() {
log("Changed: " + s.branch)
}
if name, ok := r.Created(); ok {
log("Create: " + name)
s.branches = append(append([]mujica.Branch(nil), s.branches...), mujica.Branch{Name: name})
s.branch = name
}
})
}
func gitBranchListDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "310", func() mujica.ListState[string] { return mujica.ListState[string]{} })
actions := []mujica.GitAction{{ID: "checkout", Label: "Check out", Icon: "git-branch"}, {ID: "delete", Label: "Delete", Icon: "trash-2"}}
r := mujica.BranchList(c, s, gitDemoBranches(), mujica.BranchListOptions{Actions: actions})
r.Element.Height(300)
if a, name, ok := r.Action(); ok {
log("Action: " + a + " " + name)
}
if key, ok := s.Selected(); ok && r.Changed() {
log("Selected: " + key)
}
}
func gitTagListDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "311", func() mujica.ListState[string] { return mujica.ListState[string]{} })
tags := []mujica.GitTag{
{Name: "v1.1.0", Target: "9e3779b97f4a7c15", Annotation: "Five hundred components"},
{Name: "v1.0.0", Target: "3c6ef372fe94f82a", Annotation: "The first hundred"},
{Name: "nightly", Target: "daa66d2c7ddf743f"},
}
r := mujica.TagList(c, s, tags, mujica.TagListOptions{})
r.Element.Height(220)
if _, hash, ok := r.Action(); ok {
log("Jump to " + hash[:7])
}
if key, ok := s.Selected(); ok && r.Changed() {
log("Selected: " + key)
}
}
func gitStashListDemo(c *ui.Context, log func(string)) {
type state struct {
list mujica.ListState[string]
stashes []mujica.Stash
}
s := ui.Local(c.Root(), "312", func() state {
return state{stashes: []mujica.Stash{
{Ref: "stash@{0}", Message: "WIP on main: lane colors", Branch: "main"},
{Ref: "stash@{1}", Message: "Try serif headings", Branch: "feature/type"},
{Ref: "stash@{2}", Message: "Blame hover experiment", Branch: "fix/blame"},
}}
})
r := mujica.StashList(c, &s.list, s.stashes, mujica.StashListOptions{})
r.Element.Height(240)
if a, ref, ok := r.Action(); ok {
log("Action: " + a + " " + ref)
if a != "apply" {
var keep []mujica.Stash
for _, st := range s.stashes {
if st.Ref != ref {
keep = append(keep, st)
}
}
s.stashes = keep
}
}
}
func gitBlameViewDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "313", func() mujica.BlameViewState { return mujica.BlameViewState{} })
var lines []mujica.BlameLine
for i, l := range strings.Split(strings.TrimSuffix(gitDemoNew, "\n"), "\n") {
who := []mujica.BlameLine{
{Hash: "9e3779b97f4a7c15", Author: "Sakiko", When: gitDemoTime, Summary: "Create the court package"},
{Hash: "3c6ef372fe94f82a", Author: "Uika", When: gitDemoTime.Add(-48 * time.Hour), Summary: "Greet guests by title"},
}[min(i/4%2, 1)]
who.Text = l
lines = append(lines, who)
}
mujica.BlameView(c, s, lines, mujica.BlameViewOptions{Language: "go"}).Height(380)
ui.Text(c, "Hover the left column for commit details.").TextColor(mujica.Tokens(c).TextMuted)
}
func gitFileHistoryDemo(c *ui.Context, log func(string)) {
s := ui.Local(c.Root(), "314", func() mujica.ListState[string] { return mujica.ListState[string]{} })
r := mujica.FileHistory(c, s, "court/greet.go", gitDemoCommits(12), mujica.FileHistoryOptions{})
r.Element.Height(360)
if _, hash, ok := r.Action(); ok {
log("Diff at " + hash[:7])
}
if key, ok := s.Selected(); ok && r.Changed() {
log("Selected: " + key[:7])
}
}
+116 -1
View File
@@ -1,3 +1,118 @@
package main
var mediaPages = []page{}
import (
"fmt"
"image"
"image/color"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
"mujicaui/theme"
)
const mediaCategory = "Media"
var mediaPages = []page{
{ID: "341", Name: "ImageCropper", ZhName: "图片裁剪", Category: mediaCategory, Demo: imageCropperDemo, Code: imageCropperCode,
Keys: "方向键移动裁剪框;Shift+方向键缩放;R / Shift+R 向右 / 向左旋转。"},
{ID: "342", Name: "ImageAnnotator", ZhName: "图片标注", Category: mediaCategory, Demo: imageAnnotatorDemo, Code: imageAnnotatorCode,
Keys: "[ / ] 切换选中标注;方向键移动;Delete 删除;Escape 取消选择。"},
{ID: "343", Name: "ImageThumbnail", ZhName: "缩略图", Category: mediaCategory, Demo: imageThumbnailDemo, Code: imageThumbnailCode,
Keys: "Tab 聚焦;Enter / Space 激活。"},
{ID: "344", Name: "Lightbox", ZhName: "灯箱", Category: mediaCategory, Demo: lightboxDemo, Code: lightboxCode,
Keys: "← / → 切换;Home / End 首尾;Escape 关闭并恢复焦点。"},
{ID: "345", Name: "VideoPlayer", ZhName: "视频播放器", Category: mediaCategory, Demo: videoPlayerDemo, Code: videoPlayerCode,
Keys: "画面聚焦时 Space / K 播放暂停,← / → 跳转 5 秒,F 全屏,C 字幕。"},
{ID: "346", Name: "VideoScrubber", ZhName: "视频时间轴", Category: mediaCategory, Demo: videoScrubberDemo, Code: videoScrubberCode,
Keys: "← / → 跳转一个步长;Home / End 开头结尾。"},
{ID: "347", Name: "VideoThumbnailStrip", ZhName: "缩略帧条", Category: mediaCategory, Demo: videoStripDemo, Code: videoStripCode,
Keys: "Tab 聚焦帧条;← / → 上下帧;Home / End 首尾帧。"},
{ID: "348", Name: "AudioPlayer", ZhName: "音频播放器", Category: mediaCategory, Demo: audioPlayerDemo, Code: audioPlayerCode,
Keys: "Space 播放或暂停;进度条聚焦时 ← / → 跳转 5 秒。"},
{ID: "349", Name: "AudioWaveform", ZhName: "音频波形", Category: mediaCategory, Demo: audioWaveformDemo, Code: audioWaveformCode,
Keys: "← / → 跳转 5 秒;Shift+← / → 调整选区;Escape 清除选区。"},
{ID: "350", Name: "AudioSpectrum", ZhName: "音频频谱", Category: mediaCategory, Demo: audioSpectrumDemo, Code: audioSpectrumCode,
Keys: "Tab 聚焦;← / → 逐个读出频带;Home / End 首尾。"},
{ID: "351", Name: "VolumeControl", ZhName: "音量控制", Category: mediaCategory, Demo: volumeDemo, Code: volumeCode,
Keys: "← / → 音量 ±5%;Home / End;M 静音。"},
{ID: "352", Name: "PlaybackSpeedControl", ZhName: "倍速选择", Category: mediaCategory, Demo: speedDemo, Code: speedCode,
Keys: "Enter / Space 打开;↑ / ↓ 移动;Enter 确认;Escape 关闭。"},
{ID: "353", Name: "MediaControls", ZhName: "媒体控制", Category: mediaCategory, Demo: mediaControlsDemo, Code: mediaControlsCode,
Keys: "Tab 进入;← / → 在按钮间移动;Enter / Space 按下。"},
{ID: "354", Name: "Playlist", ZhName: "播放列表", Category: mediaCategory, Demo: playlistDemo, Code: playlistCode,
Keys: "↑ / ↓ 选择;Enter 播放;Alt+↑ / ↓ 移动;Delete 移除;可拖动排序。"},
{ID: "355", Name: "SubtitleEditor", ZhName: "字幕编辑器", Category: mediaCategory, Demo: subtitleDemo, Code: subtitleCode,
Keys: "Tab 在一行的开始、结束、文本、删除间移动;时间格式 m:ss.mmm。"},
{ID: "356", Name: "ScreenRecorderControls", ZhName: "录屏控制", Category: mediaCategory, Demo: recorderDemo, Code: recorderCode,
Keys: "Tab 在按钮间移动;Enter / Space 按下。"},
{ID: "357", Name: "CameraPreview", ZhName: "摄像头预览", Category: mediaCategory, Demo: cameraDemo, Code: cameraCode,
Keys: "Tab 聚焦预览;M 切换镜像。"},
{ID: "358", Name: "MicLevelMeter", ZhName: "麦克风电平", Category: mediaCategory, Demo: micDemo, Code: micCode,
Keys: "不获取焦点;读数随描述朗读。"},
{ID: "359", Name: "DeviceSelector", ZhName: "设备选择", Category: mediaCategory, Demo: deviceDemo, Code: deviceCode,
Keys: "Tab 在选择框间移动;Enter 打开;输入字母跳转。"},
{ID: "360", Name: "ImageCompare", ZhName: "前后对比", Category: mediaCategory, Demo: compareDemo, Code: compareCode,
Keys: "← / → 移动 1%;Home / End;可拖动。"},
}
// mediaSection titles a part of a demo.
func mediaSection(c *ui.Context, title string, fn func()) {
ui.Column(c).Gap(8).FillWidth().Children(func() {
ui.Text(c, title).FontSize(mujica.FontSize(c, theme.CaptionSize)).TextColor(mujica.Tokens(c).TextMuted)
fn()
})
}
// mediaCheck is a parameter checkbox that logs its changes.
func mediaCheck(c *ui.Context, log func(string), v *bool, label string) {
before := *v
mujica.Checkbox(c, v, label, mujica.CheckboxOptions{})
if *v != before {
log(fmt.Sprintf("%s = %v", label, *v))
}
}
// mediaPicture draws a w×h court scene in variant hues.
func mediaPicture(w, h, variant int) *image.RGBA {
walls := []color.RGBA{{36, 28, 32, 255}, {242, 235, 221, 255}, {73, 42, 54, 255}}
arch := []color.RGBA{{125, 32, 52, 255}, {173, 145, 100, 255}, {53, 94, 128, 255}}
img := image.NewRGBA(image.Rect(0, 0, w, h))
cx := float64(w) / 2
for y := range h {
for x := range w {
c := walls[variant%3]
dx, dy := (float64(x)-cx)/(float64(w)*0.28), (float64(y)-float64(h)*0.55)/(float64(h)*0.5)
if dy > 0 && math.Abs(dx) < 1 || dx*dx+dy*dy < 1 {
c = arch[variant%3]
}
if x%(w/6) < w/40 || y > h*7/8 {
c = color.RGBA{189, 161, 114, 255}
}
if math.Abs(float64(x)-cx)/float64(w)+math.Abs(float64(y)-float64(h)*0.45)/float64(h) < 0.06 {
c = color.RGBA{251, 247, 239, 255}
}
img.Set(x, y, c)
}
}
return img
}
var (
mediaPhotos = []*image.RGBA{mediaPicture(480, 270, 0), mediaPicture(480, 270, 1), mediaPicture(360, 360, 2)}
mediaBitmaps = func() []*ui.Bitmap {
out := make([]*ui.Bitmap, len(mediaPhotos))
for i, p := range mediaPhotos {
out[i] = ui.NewBitmap(p)
}
return out
}()
)
// mediaClockLabel writes d as m:ss for event logs.
func mediaClockLabel(d time.Duration) string {
s := int(d / time.Second)
return fmt.Sprintf("%d:%02d", s/60, s%60)
}
+191
View File
@@ -0,0 +1,191 @@
package main
import (
"fmt"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
// mediaClockState simulates the caller's playback: position advances while playing.
type mediaClockState struct {
pos, dur time.Duration
playing bool
rate float64
last time.Time
}
// tick advances the position; frames are asked only while playing.
func (m *mediaClockState) tick(c *ui.Context) {
now := c.Now()
if m.playing {
if !m.last.IsZero() {
m.pos += time.Duration(float64(now.Sub(m.last)) * m.rate)
}
if m.pos >= m.dur {
m.pos, m.playing = m.dur, false
} else {
c.AnimationFrame()
}
}
m.last = now
}
func videoPlayerDemo(c *ui.Context, log func(string)) {
type state struct {
clock mediaClockState
subs, full, pip, noFrame bool
disabled bool
}
s := ui.Local(c.Root(), "345", func() state {
return state{clock: mediaClockState{dur: 3 * time.Minute, rate: 1}, subs: true}
})
s.clock.tick(c)
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
mediaCheck(c, log, &s.noFrame, "No frame")
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
cues := []string{"The curtain rises.", "Welcome to the masquerade.", "Ave Mujica."}
var frame ui.ImageSource = mediaBitmaps[int(s.clock.pos/(20*time.Second))%3]
if s.noFrame {
frame = nil
}
st := mujica.VideoState{Frame: frame, Position: s.clock.pos, Duration: s.clock.dur, Playing: s.clock.playing,
Rate: s.clock.rate, Buffered: []mujica.MediaRange{{Start: 0, End: s.clock.pos + 40*time.Second}},
Subtitle: cues[int(s.clock.pos/(10*time.Second))%3], SubtitlesOn: s.subs, Fullscreen: s.full, PiP: s.pip}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(640).Children(func() {
mujica.VideoPlayer(c, st, mujica.VideoPlayerOptions{
OnPlay: func() { s.clock.playing = true; log("play") },
OnPause: func() { s.clock.playing = false; log("pause") },
OnSeek: func(d time.Duration) { s.clock.pos = d; log("seek " + mediaClockLabel(d)) },
OnRate: func(r float64) { s.clock.rate = r; log(fmt.Sprintf("rate %v×", r)) },
OnSubtitles: func(on bool) { s.subs = on; log(fmt.Sprintf("subtitles %v", on)) },
OnFullscreen: func(on bool) { s.full = on; log(fmt.Sprintf("full screen requested %v", on)) },
OnPiP: func(on bool) { s.pip = on; log(fmt.Sprintf("picture in picture requested %v", on)) },
Disabled: s.disabled,
})
})
})
}
func videoScrubberDemo(c *ui.Context, log func(string)) {
type state struct {
pos time.Duration
preview, disabled bool
}
s := ui.Local(c.Root(), "346", func() state { return state{pos: 40 * time.Second, preview: true} })
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
mediaCheck(c, log, &s.preview, "Preview slot")
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
opts := mujica.VideoScrubberOptions{Duration: 3 * time.Minute, Disabled: s.disabled,
Buffered: []mujica.MediaRange{{Start: 0, End: 90 * time.Second}, {Start: 120 * time.Second, End: 140 * time.Second}}}
if s.preview {
opts.Preview = func(at time.Duration) {
ui.Image(c, mediaBitmaps[int(at/(time.Minute))%3]).Fit(ui.Cover).Fill()
}
}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(560).Padding(110, 0, 0, 0).Children(func() {
if mujica.VideoScrubber(c, &s.pos, opts).Changed() {
log("seek " + mediaClockLabel(s.pos))
}
})
})
}
func audioPlayerDemo(c *ui.Context, log func(string)) {
type state struct {
clock mediaClockState
disabled bool
}
s := ui.Local(c.Root(), "348", func() state { return state{clock: mediaClockState{dur: 4*time.Minute + 5*time.Second, rate: 1}} })
s.clock.tick(c)
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(560).Children(func() {
mujica.AudioPlayer(c, mujica.AudioState{Title: "Ave Mujica — Overture", Position: s.clock.pos, Duration: s.clock.dur, Playing: s.clock.playing},
mujica.AudioPlayerOptions{
OnPlay: func() { s.clock.playing = true; log("play") },
OnPause: func() { s.clock.playing = false; log("pause") },
OnSeek: func(d time.Duration) { s.clock.pos = d; log("seek " + mediaClockLabel(d)) },
Disabled: s.disabled,
})
})
})
}
// mediaSamples is a synthetic voice-like recording, made once.
var mediaSamples = func() []float32 {
s := make([]float32, 120000)
for i := range s {
env := 0.25 + 0.7*math.Abs(math.Sin(float64(i)/9000))*math.Abs(math.Sin(float64(i)/2100))
s[i] = float32(env * math.Sin(float64(i)/7))
}
return s
}()
func audioWaveformDemo(c *ui.Context, log func(string)) {
type state struct {
pos time.Duration
sel mujica.MediaRange
status int
selecting bool
}
s := ui.Local(c.Root(), "349", func() state { return state{pos: 20 * time.Second, selecting: true} })
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
before := s.status
mujica.SegmentedControl(c, &s.status, []mujica.Segment{{Label: "Ready"}, {Label: "Loading"}, {Label: "Failed"}, {Label: "Empty"}}, mujica.SegmentedControlOptions{Label: "Status"})
if s.status != before {
log(fmt.Sprintf("status %d", s.status))
}
mediaCheck(c, log, &s.selecting, "Selectable")
})
})
opts := mujica.AudioWaveformOptions{Duration: time.Minute, Position: s.pos, OnSeek: func(d time.Duration) { s.pos = d; log("seek " + mediaClockLabel(d)) }}
if s.selecting {
opts.Selection = &s.sel
}
data := mediaSamples
switch s.status {
case 1:
opts.Status, data = mujica.DataLoading, nil
case 2:
opts.Status, opts.Error, data = mujica.DataFailed, "The recording could not be decoded.", nil
case 3:
data = nil
}
mediaSection(c, "Live", func() {
if mujica.AudioWaveform(c, data, opts).Changed() {
log(fmt.Sprintf("selection %s – %s", mediaClockLabel(s.sel.Start), mediaClockLabel(s.sel.End)))
}
})
}
func audioSpectrumDemo(c *ui.Context, log func(string)) {
type state struct{ live bool }
s := ui.Local(c.Root(), "350", func() state { return state{live: true} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.live, "Live input") })
freqs := []string{"32 Hz", "63 Hz", "125 Hz", "250 Hz", "500 Hz", "1 kHz", "2 kHz", "4 kHz", "8 kHz", "16 kHz"}
bands := make([]float32, len(freqs))
t := float64(c.Now().UnixMilli()) / 1000
for i := range bands {
bands[i] = float32(0.2 + 0.6*math.Abs(math.Sin(float64(i)*0.9+t*(1+float64(i)*0.2))))
}
if s.live {
// The demo is its own analyser; the component only draws.
c.AnimationFrame()
}
mediaSection(c, "Live", func() {
mujica.AudioSpectrum(c, bands, mujica.AudioSpectrumOptions{BandLabel: func(i int) string { return freqs[i] }})
})
}
+178
View File
@@ -0,0 +1,178 @@
package main
// Media page snippets, each its compiled Example's body.
const (
imageCropperCode = `crop := mujica.ImageCrop{} // empty: starts fitted to the image
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ImageCropper(c, examplePhoto, &crop, mujica.ImageCropperOptions{Aspect: 1}).Changed() {
_ = crop.Rect // in pixels of the photo turned by crop.Rotation
}
}
ui.Render(view, 400, 300, 1)`
imageAnnotatorCode = `photo := ui.NewBitmap(examplePhoto)
var notes []mujica.Annotation
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ImageAnnotator(c, photo, &notes, mujica.ImageAnnotatorOptions{}).Changed() {
data, _ := mujica.ExportAnnotations(notes)
_ = data // save the JSON
}
}
ui.Render(view, 400, 300, 1)`
imageThumbnailCode = `photo := ui.NewBitmap(examplePhoto)
selected := false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.ImageThumbnail(c, photo, mujica.ImageThumbnailOptions{Alt: "Grand hall", Aspect: 4.0 / 3, Selected: selected}).Changed() {
selected = !selected
}
}
ui.Render(view, 400, 300, 1)`
lightboxCode = `images := []mujica.ViewerImage{{Source: ui.NewBitmap(examplePhoto), Alt: "The grand hall"}}
open, index := false, 0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.Button(c, "View photos", mujica.ButtonOptions{}).Clicked() {
open = true
}
mujica.Lightbox(c, &open, &index, images)
}
ui.Render(view, 400, 300, 1)`
videoPlayerCode = `st := mujica.VideoState{Duration: 4 * time.Minute, Rate: 1}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.VideoPlayer(c, st, mujica.VideoPlayerOptions{
OnPlay: func() { st.Playing = true }, // start your decoder
OnPause: func() { st.Playing = false }, // pause it
OnSeek: func(d time.Duration) { st.Position = d },
OnRate: func(r float64) { st.Rate = r },
})
}
ui.Render(view, 400, 300, 1)`
videoScrubberCode = `pos := 30 * time.Second
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.VideoScrubber(c, &pos, mujica.VideoScrubberOptions{
Duration: 2 * time.Minute,
Buffered: []mujica.MediaRange{{Start: 0, End: time.Minute}},
})
}
ui.Render(view, 400, 300, 1)`
videoStripCode = `frames := []mujica.StripFrame{{At: 0}, {At: 10 * time.Second}, {At: 20 * time.Second}}
current := 0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.VideoThumbnailStrip(c, &current, frames, mujica.VideoThumbnailStripOptions{}).Changed() {
_ = frames[current].At // seek the player there
}
}
ui.Render(view, 400, 300, 1)`
audioPlayerCode = `st := mujica.AudioState{Title: "Overture", Duration: 3 * time.Minute}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AudioPlayer(c, st, mujica.AudioPlayerOptions{
OnPlay: func() { st.Playing = true },
OnPause: func() { st.Playing = false },
OnSeek: func(d time.Duration) { st.Position = d },
})
}
ui.Render(view, 400, 300, 1)`
audioWaveformCode = `samples := make([]float32, 48000) // from your decoder, -1 to 1
pos, sel := time.Duration(0), mujica.MediaRange{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AudioWaveform(c, samples, mujica.AudioWaveformOptions{
Duration: time.Second, Position: pos, Selection: &sel,
OnSeek: func(d time.Duration) { pos = d },
})
}
ui.Render(view, 400, 300, 1)`
audioSpectrumCode = `bands := []float32{0.2, 0.6, 0.9, 0.4, 0.1} // set by your analyser, then c.Invalidate()
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.AudioSpectrum(c, bands, mujica.AudioSpectrumOptions{})
}
ui.Render(view, 400, 300, 1)`
volumeCode = `volume, muted := 0.8, false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.VolumeControl(c, &volume, &muted, mujica.VolumeControlOptions{})
}
ui.Render(view, 400, 300, 1)`
speedCode = `rate := 1.0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PlaybackSpeedControl(c, &rate, mujica.PlaybackSpeedControlOptions{})
}
ui.Render(view, 400, 300, 1)`
mediaControlsCode = `st := mujica.MediaControlsState{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.MediaControls(c, st, mujica.MediaControlsOptions{
OnPrevious: func() {}, OnNext: func() {},
OnPlay: func() { st.Playing = true }, OnPause: func() { st.Playing = false },
OnLoop: func(m mujica.LoopMode) { st.Loop = m }, OnShuffle: func(on bool) { st.Shuffle = on },
})
}
ui.Render(view, 400, 300, 1)`
playlistCode = `items := []mujica.PlaylistItem{{ID: "a", Title: "Overture"}, {ID: "b", Title: "Masquerade"}}
current := "a"
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if r := mujica.Playlist(c, &items, mujica.PlaylistOptions{Current: current}); r.Played != "" {
current = r.Played
}
}
ui.Render(view, 400, 300, 1)`
subtitleCode = `cues := []mujica.SubtitleCue{{Start: 0, End: 2 * time.Second, Text: "The curtain rises."}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SubtitleEditor(c, &cues, mujica.SubtitleEditorOptions{})
if len(mujica.CheckSubtitles(cues)) > 0 {
_ = cues // block saving until the issues are fixed
}
}
ui.Render(view, 400, 300, 1)`
recorderCode = `st := mujica.RecorderState{}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ScreenRecorderControls(c, st, mujica.ScreenRecorderControlsOptions{
OnStart: func() { st.Status = mujica.RecorderRecording },
OnPause: func() { st.Status = mujica.RecorderPaused },
OnResume: func() { st.Status = mujica.RecorderRecording },
OnStop: func() { st = mujica.RecorderState{} },
})
}
ui.Render(view, 400, 300, 1)`
cameraCode = `frame := mujica.CameraFrame{Image: examplePhoto, Seq: 1} // a new Seq for each captured frame
mirror := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.CameraPreview(c, frame, &mirror, mujica.CameraPreviewOptions{})
}
ui.Render(view, 400, 300, 1)`
micCode = `level := float32(0.4) // the input's peak, 0 to 1 of full scale
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.MicLevelMeter(c, level, mujica.MicLevelMeterOptions{})
}
ui.Render(view, 400, 300, 1)`
deviceCode = `devices := mujica.MediaDevices{
Microphones: []mujica.MediaDevice{{ID: "mic-1", Name: "Built-in Microphone"}},
Speakers: []mujica.MediaDevice{{ID: "out-1", Name: "Headphones"}},
}
sel := mujica.DeviceSelection{Microphone: "mic-1"}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.DeviceSelector(c, &sel, devices, mujica.DeviceSelectorOptions{})
}
ui.Render(view, 400, 300, 1)`
compareCode = `before, after := ui.NewBitmap(examplePhoto), ui.NewBitmap(examplePhoto)
split := 0.5
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.ImageCompare(c, before, after, &split, mujica.ImageCompareOptions{})
}
ui.Render(view, 400, 300, 1)`
)
+225
View File
@@ -0,0 +1,225 @@
package main
import (
"fmt"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func volumeDemo(c *ui.Context, log func(string)) {
type state struct {
volume float64
muted, disabled bool
}
s := ui.Local(c.Root(), "351", func() state { return state{volume: 0.7} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
if mujica.VolumeControl(c, &s.volume, &s.muted, mujica.VolumeControlOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("volume %.0f%%, muted %v", s.volume*100, s.muted))
}
})
}
func speedDemo(c *ui.Context, log func(string)) {
type state struct {
rate float64
disabled bool
}
s := ui.Local(c.Root(), "352", func() state { return state{rate: 1} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
if mujica.PlaybackSpeedControl(c, &s.rate, mujica.PlaybackSpeedControlOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("rate %v×", s.rate))
}
})
}
func mediaControlsDemo(c *ui.Context, log func(string)) {
type state struct {
st mujica.MediaControlsState
track int
disabled bool
}
s := ui.Local(c.Root(), "353", func() state { return state{} })
tracks := []string{"Ave Mujica", "Imprisoned XII", "KiLLKiSS", "Symbol I: △"}
s.st.NoPrevious, s.st.NoNext = s.track == 0, s.track == len(tracks)-1
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
ui.Text(c, fmt.Sprintf("Now: %s (%d of %d)", tracks[s.track], s.track+1, len(tracks)))
mujica.MediaControls(c, s.st, mujica.MediaControlsOptions{
OnPrevious: func() { s.track--; log("previous") },
OnNext: func() { s.track++; log("next") },
OnPlay: func() { s.st.Playing = true; log("play") },
OnPause: func() { s.st.Playing = false; log("pause") },
OnLoop: func(m mujica.LoopMode) { s.st.Loop = m; log(fmt.Sprintf("loop mode %d", m)) },
OnShuffle: func(on bool) { s.st.Shuffle = on; log(fmt.Sprintf("shuffle %v", on)) },
Disabled: s.disabled,
})
})
}
func playlistDemo(c *ui.Context, log func(string)) {
type state struct {
items []mujica.PlaylistItem
current string
disabled bool
}
s := ui.Local(c.Root(), "354", func() state {
titles := []string{"Ave Mujica", "Imprisoned XII", "KiLLKiSS", "Symbol I: △", "Choir 'S' Choir", "Georgette Me, Georgette You"}
items := make([]mujica.PlaylistItem, len(titles))
for i, t := range titles {
items[i] = mujica.PlaylistItem{ID: fmt.Sprint("t", i), Title: t, Subtitle: fmt.Sprintf("Act %d", i+1), Duration: time.Duration(200+i*17) * time.Second}
}
return state{items: items, current: "t1"}
})
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Add track", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
id := fmt.Sprint("t", len(s.items)+int(time.Now().UnixNano()%1000000))
s.items = append(s.items, mujica.PlaylistItem{ID: id, Title: "Encore " + id, Duration: 3 * time.Minute})
log("added " + id)
}
})
})
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(520).Children(func() {
r := mujica.Playlist(c, &s.items, mujica.PlaylistOptions{Current: s.current, Disabled: s.disabled})
switch {
case r.Played != "":
s.current = r.Played
log("play " + r.Played)
case r.Removed != "":
log("removed " + r.Removed)
case r.Moved:
log("reordered")
}
})
})
}
func subtitleDemo(c *ui.Context, log func(string)) {
type state struct {
cues []mujica.SubtitleCue
disabled bool
}
s := ui.Local(c.Root(), "355", func() state {
return state{cues: []mujica.SubtitleCue{
{Start: 0, End: 2500 * time.Millisecond, Text: "The curtain rises."},
{Start: 2 * time.Second, End: 5 * time.Second, Text: "Welcome to the masquerade."},
{Start: 6 * time.Second, End: 9 * time.Second, Text: "Ave Mujica."},
}}
})
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(720).Children(func() {
if mujica.SubtitleEditor(c, &s.cues, mujica.SubtitleEditorOptions{Height: 260, Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("%d cues, %d issues", len(s.cues), len(mujica.CheckSubtitles(s.cues))))
}
})
})
}
func recorderDemo(c *ui.Context, log func(string)) {
type state struct {
st mujica.RecorderState
since time.Time
disabled bool
accumulated time.Duration
}
s := ui.Local(c.Root(), "356", func() state { return state{} })
if s.st.Status == mujica.RecorderRecording {
s.st.Elapsed = s.accumulated + c.Now().Sub(s.since)
// The clock changes each second; ask for that frame only.
c.After(time.Second - s.st.Elapsed%time.Second)
}
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
mujica.ScreenRecorderControls(c, s.st, mujica.ScreenRecorderControlsOptions{
OnStart: func() {
s.st, s.since, s.accumulated = mujica.RecorderState{Status: mujica.RecorderRecording}, c.Now(), 0
log("start")
},
OnPause: func() { s.st.Status, s.accumulated = mujica.RecorderPaused, s.st.Elapsed; log("pause") },
OnResume: func() { s.st.Status, s.since = mujica.RecorderRecording, c.Now(); log("resume") },
OnStop: func() { log("stop at " + mediaClockLabel(s.st.Elapsed)); s.st = mujica.RecorderState{} },
Disabled: s.disabled,
})
})
}
func cameraDemo(c *ui.Context, log func(string)) {
type state struct {
mirror, signal, disabled bool
}
s := ui.Local(c.Root(), "357", func() state { return state{mirror: true, signal: true} })
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
mediaCheck(c, log, &s.signal, "Signal")
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
frame := mujica.CameraFrame{}
if s.signal {
frame = mujica.CameraFrame{Image: mediaPhotos[2], Seq: 1}
}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(480).Children(func() {
if mujica.CameraPreview(c, frame, &s.mirror, mujica.CameraPreviewOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("mirror %v", s.mirror))
}
})
})
}
func micDemo(c *ui.Context, log func(string)) {
type state struct{ live bool }
s := ui.Local(c.Root(), "358", func() state { return state{live: true} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.live, "Live input") })
level := float32(0.3)
if s.live {
t := float64(c.Now().UnixMilli()) / 1000
level = float32(math.Min(1, 0.15+0.8*math.Abs(math.Sin(t*1.3)*math.Sin(t*0.37))))
c.AnimationFrame()
}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(420).Children(func() { mujica.MicLevelMeter(c, level, mujica.MicLevelMeterOptions{}) })
})
}
func deviceDemo(c *ui.Context, log func(string)) {
type state struct {
sel mujica.DeviceSelection
noCamera, disabled bool
}
s := ui.Local(c.Root(), "359", func() state { return state{sel: mujica.DeviceSelection{Microphone: "m1", Speaker: "s2"}} })
devices := mujica.MediaDevices{
Microphones: []mujica.MediaDevice{{ID: "m1", Name: "Built-in Microphone"}, {ID: "m2", Name: "Studio Condenser"}},
Cameras: []mujica.MediaDevice{{ID: "c1", Name: "FaceTime HD Camera"}},
Speakers: []mujica.MediaDevice{{ID: "s1", Name: "Built-in Speakers"}, {ID: "s2", Name: "Headphones"}},
}
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
before := s.noCamera
mediaCheck(c, log, &s.noCamera, "No cameras")
if s.noCamera != before {
s.sel.Camera = ""
}
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
if s.noCamera {
devices.Cameras = []mujica.MediaDevice{}
}
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(480).Children(func() {
if mujica.DeviceSelector(c, &s.sel, devices, mujica.DeviceSelectorOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("microphone %q, camera %q, speaker %q", s.sel.Microphone, s.sel.Camera, s.sel.Speaker))
}
})
})
}
+162
View File
@@ -0,0 +1,162 @@
package main
import (
"errors"
"fmt"
"image"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
func imageCropperDemo(c *ui.Context, log func(string)) {
type state struct {
crop mujica.ImageCrop
aspect int
disabled bool
}
s := ui.Local(c.Root(), "341", func() state { return state{} })
aspects := []float64{0, 1, 16.0 / 9, 4.0 / 3}
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
before := s.aspect
mujica.SegmentedControl(c, &s.aspect, []mujica.Segment{{Label: "Free"}, {Label: "1:1"}, {Label: "16:9"}, {Label: "4:3"}}, mujica.SegmentedControlOptions{Label: "Aspect"})
if s.aspect != before {
s.crop.Rect = image.Rectangle{} // refit to the new ratio
log(fmt.Sprintf("aspect = %v", aspects[s.aspect]))
}
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
mediaSection(c, "Live", func() {
if mujica.ImageCropper(c, mediaPhotos[0], &s.crop, mujica.ImageCropperOptions{Aspect: aspects[s.aspect], Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("crop %v rotated %d°", s.crop.Rect, s.crop.Rotation))
}
})
}
func imageAnnotatorDemo(c *ui.Context, log func(string)) {
type state struct {
notes []mujica.Annotation
disabled bool
}
s := ui.Local(c.Root(), "342", func() state {
return state{notes: []mujica.Annotation{{Kind: mujica.AnnotateRect, From: image.Pt(150, 60), To: image.Pt(330, 230)}}}
})
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).AlignItems(ui.Center).Children(func() {
mediaCheck(c, log, &s.disabled, "Disabled")
if mujica.Button(c, "Export JSON", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
data, err := mujica.ExportAnnotations(s.notes)
if err != nil {
log("export failed: " + err.Error())
} else {
c.WriteClipboard(string(data))
log(fmt.Sprintf("exported %d bytes to the clipboard", len(data)))
}
}
})
})
mediaSection(c, "Live", func() {
if mujica.ImageAnnotator(c, mediaBitmaps[1], &s.notes, mujica.ImageAnnotatorOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("annotations: %d", len(s.notes)))
}
})
}
func imageThumbnailDemo(c *ui.Context, log func(string)) {
type state struct {
selected int
loading, failed, disabled bool
}
s := ui.Local(c.Root(), "343", func() state { return state{} })
mediaSection(c, "Parameters", func() {
ui.Row(c).Gap(16).Children(func() {
mediaCheck(c, log, &s.loading, "Loading")
mediaCheck(c, log, &s.failed, "Failed")
mediaCheck(c, log, &s.disabled, "Disabled")
})
})
mediaSection(c, "Live", func() {
ui.Row(c).Gap(12).Wrap().Children(func() {
for i, b := range mediaBitmaps {
var err error
if s.failed && i == 2 {
err = errors.New("the file could not be read")
}
o := mujica.ImageThumbnailOptions{Alt: fmt.Sprintf("Scene %d", i+1), Aspect: 4.0 / 3, Width: 120,
Selected: s.selected == i, Loading: s.loading && i == 1, Err: err, Disabled: s.disabled}
if mujica.ImageThumbnail(c, b, o).Changed() {
s.selected = i
log(fmt.Sprintf("selected scene %d", i+1))
}
}
})
})
}
func lightboxDemo(c *ui.Context, log func(string)) {
type state struct {
open bool
index int
}
s := ui.Local(c.Root(), "344", func() state { return state{} })
images := []mujica.ViewerImage{
{Source: mediaBitmaps[0], Alt: "The grand hall at dusk"},
{Source: mediaBitmaps[1], Alt: "The ivory gallery"},
{Source: mediaBitmaps[2], Alt: "The blue chapel"},
}
mediaSection(c, "Live", func() {
ui.Row(c).Gap(12).Children(func() {
for i, im := range images {
if mujica.ImageThumbnail(c, im.Source, mujica.ImageThumbnailOptions{Alt: im.Alt, Aspect: 4.0 / 3}).Changed() {
s.open, s.index = true, i
log(fmt.Sprintf("open at %d", i+1))
}
}
})
was := s.open
if mujica.Lightbox(c, &s.open, &s.index, images).Changed() {
log(fmt.Sprintf("image %d", s.index+1))
}
if was && !s.open {
log("closed")
}
})
}
func videoStripDemo(c *ui.Context, log func(string)) {
type state struct {
current int
disabled bool
}
s := ui.Local(c.Root(), "347", func() state { return state{} })
frames := make([]mujica.StripFrame, 240)
for i := range frames {
frames[i] = mujica.StripFrame{Image: mediaBitmaps[i%3], At: time.Duration(i) * 5 * time.Second}
}
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
if mujica.VideoThumbnailStrip(c, &s.current, frames, mujica.VideoThumbnailStripOptions{Disabled: s.disabled}).Changed() {
log("jump to " + mediaClockLabel(frames[s.current].At))
}
})
}
func compareDemo(c *ui.Context, log func(string)) {
type state struct {
split float64
disabled bool
}
s := ui.Local(c.Root(), "360", func() state { return state{split: 0.5} })
mediaSection(c, "Parameters", func() { mediaCheck(c, log, &s.disabled, "Disabled") })
mediaSection(c, "Live", func() {
ui.Column(c).MaxWidth(560).Children(func() {
if mujica.ImageCompare(c, mediaBitmaps[0], mediaBitmaps[1], &s.split, mujica.ImageCompareOptions{Disabled: s.disabled}).Changed() {
log(fmt.Sprintf("split %.0f%%", s.split*100))
}
})
})
}
+274 -1
View File
@@ -1,3 +1,276 @@
package main
var tradingPages = []page{}
import (
"fmt"
"math"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
const tradingCategory = "Trading"
// tradingTrend shows and applies the rise-color switch.
func tradingTrend(c *ui.Context, redUp *bool, log func(string)) {
if mujica.Checkbox(c, redUp, "Red means up (China)", mujica.CheckboxOptions{}).Changed() {
log(fmt.Sprintf("red up %v", *redUp))
}
t := mujica.GreenUp
if *redUp {
t = mujica.RedUp
}
mujica.UseTrend(c, t)
}
// tradingAction is a demo button at its natural width.
func tradingAction(c *ui.Context, label string) bool {
clicked := false
ui.Row(c).Children(func() {
clicked = mujica.Button(c, label, mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked()
})
return clicked
}
// tradingBook is a deterministic book around mid, moved by step.
func tradingBook(mid float64, step int) mujica.OrderBookData {
var b mujica.OrderBookData
for i := range 12 {
w := float64((i*7+step*3)%11 + 1)
b.Bids = append(b.Bids, mujica.BookLevel{Price: math.Round((mid-0.01*float64(i+1))*100) / 100, Size: w * 10})
b.Asks = append(b.Asks, mujica.BookLevel{Price: math.Round((mid+0.01*float64(i))*100) / 100, Size: float64((i*5+step)%9+1) * 10})
}
b.Version = step
return b
}
var tradingPages = []page{
{ID: "161", Name: "TickerTape", ZhName: "行情滚动条", Category: tradingCategory, Code: tradingCode["TickerTape"], Demo: tradingTickerDemo,
Keys: "Tab focuses the tape, which pauses it. With reduced motion, Left and Right turn pages."},
{ID: "162", Name: "QuoteCard", ZhName: "报价卡片", Category: tradingCategory, Code: tradingCode["QuoteCard"], Demo: tradingQuoteDemo,
Keys: "The card takes no focus; controls in its chart slot do."},
{ID: "163", Name: "PriceText", ZhName: "价格文本", Category: tradingCategory, Code: tradingCode["PriceText"], Demo: tradingPriceDemo,
Keys: "Display only."},
{ID: "164", Name: "PriceChangeBadge", ZhName: "涨跌徽标", Category: tradingCategory, Code: tradingCode["PriceChangeBadge"], Demo: tradingBadgeDemo,
Keys: "Display only; the name reads the direction and values."},
{ID: "165", Name: "OrderBook", ZhName: "盘口", Category: tradingCategory, Code: tradingCode["OrderBook"], Demo: tradingBookDemo,
Keys: "Tab into the levels, Up and Down move, Enter picks a price."},
{ID: "166", Name: "Level2Quotes", ZhName: "多档行情", Category: tradingCategory, Code: tradingCode["Level2Quotes"], Demo: tradingLevel2Demo,
Keys: "Display only; each row reads its level, price, size and orders."},
{ID: "167", Name: "TimeAndSales", ZhName: "逐笔成交", Category: tradingCategory, Code: tradingCode["TimeAndSales"], Demo: tradingTapeDemo,
Keys: "Tab to Pause and the list; Up and Down move in the list."},
{ID: "168", Name: "DepthLadder", ZhName: "价格阶梯", Category: tradingCategory, Code: tradingCode["DepthLadder"], Demo: tradingLadderDemo,
Keys: "Arrows move between cells, Enter orders, Page Up and Page Down scroll, the wheel steps a tick."},
{ID: "169", Name: "SpreadIndicator", ZhName: "价差指示", Category: tradingCategory, Code: tradingCode["SpreadIndicator"], Demo: tradingSpreadDemo,
Keys: "Display only."},
{ID: "170", Name: "BidAskBar", ZhName: "买卖力量条", Category: tradingCategory, Code: tradingCode["BidAskBar"], Demo: tradingBarDemo,
Keys: "Display only; read as a meter."},
{ID: "171", Name: "OrderEntry", ZhName: "下单面板", Category: tradingCategory, Code: tradingCode["OrderEntry"], Demo: tradingEntryDemo,
Keys: "Tab through side, type and fields; Up and Down step numbers; a failed submit focuses the first error."},
{ID: "172", Name: "QuickTradeButtons", ZhName: "快速交易按钮", Category: tradingCategory, Code: tradingCode["QuickTradeButtons"], Demo: tradingQuickDemo,
Keys: "Enter or Space trades; with Confirm, Enter in the popup cancels and Escape closes it."},
{ID: "173", Name: "PositionTable", ZhName: "持仓表", Category: tradingCategory, Code: tradingCode["PositionTable"], Demo: tradingPositionsDemo,
Keys: "DataTable keys; Tab reaches each Close button."},
{ID: "174", Name: "OrderTable", ZhName: "委托表", Category: tradingCategory, Code: tradingCode["OrderTable"], Demo: tradingOrdersDemo,
Keys: "Arrows switch the status filter; DataTable keys; Tab reaches Cancel."},
{ID: "175", Name: "TradeHistoryTable", ZhName: "成交记录表", Category: tradingCategory, Code: tradingCode["TradeHistoryTable"], Demo: tradingHistoryDemo,
Keys: "Tab to the range picker, Export and the table."},
{ID: "176", Name: "PnLDisplay", ZhName: "盈亏显示", Category: tradingCategory, Code: tradingCode["PnLDisplay"], Demo: tradingPnLDemo,
Keys: "Tab to the eye button; Enter or Space masks the amounts."},
{ID: "177", Name: "LeverageSlider", ZhName: "杠杆滑块", Category: tradingCategory, Code: tradingCode["LeverageSlider"], Demo: tradingLeverageDemo,
Keys: "Arrows step levels, Home and End jump; Apply sets the value."},
{ID: "178", Name: "MarginIndicator", ZhName: "保证金率指示", Category: tradingCategory, Code: tradingCode["MarginIndicator"], Demo: tradingMarginDemo,
Keys: "Display only; read as a meter with its lines."},
{ID: "179", Name: "RiskMeter", ZhName: "风险等级", Category: tradingCategory, Code: tradingCode["RiskMeter"], Demo: tradingRiskDemo,
Keys: "Display only."},
{ID: "180", Name: "OrderConfirmDialog", ZhName: "下单确认对话框", Category: tradingCategory, Code: tradingCode["OrderConfirmDialog"], Demo: tradingConfirmDemo,
Keys: "Cancel has the focus; Escape cancels; the backdrop does not close it."},
}
type tradingTickerState struct {
redUp bool
items []mujica.TickerItem
}
func tradingTickerDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "161", func() tradingTickerState {
return tradingTickerState{items: []mujica.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}, {Symbol: "OBLV", Price: 1204.5, ChangePercent: 0.4},
{Symbol: "DOLO", Price: 7.25, ChangePercent: -0.8}, {Symbol: "MORT", Price: 33.1, ChangePercent: 1.2}, {Symbol: "TIMR", Price: 61.9, ChangePercent: -2.3}}}
})
tradingTrend(c, &d.redUp, log)
if tradingAction(c, "Move prices") {
for i := range d.items {
d.items[i].ChangePercent = -d.items[i].ChangePercent
}
log("prices moved")
}
ui.Box(c).Width(560).Children(func() { mujica.TickerTape(c, d.items, mujica.TickerTapeOptions{}) })
formStates(c, func() {
ui.Box(c).Width(280).Children(func() { mujica.TickerTape(c, d.items[:2], mujica.TickerTapeOptions{Speed: 15}) })
})
}
type tradingQuoteState struct {
redUp bool
price float64
}
func tradingQuoteDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "162", func() tradingQuoteState { return tradingQuoteState{price: 412.08} })
tradingTrend(c, &d.redUp, log)
if mujica.Slider(c, &d.price, mujica.SliderOptions{Min: 380, Max: 440, Step: 0.01, Label: "Price", ShowValue: true}).Changed() {
log(fmt.Sprintf("price %.2f", d.price))
}
change := d.price - 417.7
ui.Row(c).Gap(16).Wrap().Children(func() {
mujica.QuoteCard(c, mujica.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: d.price, Change: change, ChangePercent: change / 417.7 * 100},
mujica.QuoteCardOptions{Chart: func() {
k := mujica.Tokens(c)
ui.Box(c).Fill().Draw(func(p *ui.Painter, r ui.Rect) {
p.Line(r.X, r.Y+r.H*0.7, r.X+r.W/2, r.Y+r.H*0.3, 1.5, k.Info)
p.Line(r.X+r.W/2, r.Y+r.H*0.3, r.X+r.W, r.Y+r.H*(0.5-float32(change)/40), 1.5, k.Info)
})
}})
mujica.QuoteCard(c, mujica.Quote{Symbol: "AVE", Price: 88.2}, mujica.QuoteCardOptions{})
})
}
type tradingPriceState struct {
redUp, noFlash, grouping bool
price float64
}
func tradingPriceDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "163", func() tradingPriceState { return tradingPriceState{price: 18234.5, grouping: true} })
tradingTrend(c, &d.redUp, log)
formToggles(c, []string{"Thousands separators", "No flash"}, &d.grouping, &d.noFlash)
ui.Row(c).Gap(8).Children(func() {
if mujica.Button(c, "Tick up", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
d.price += 12.5
log(fmt.Sprintf("price %.2f", d.price))
}
if mujica.Button(c, "Tick down", mujica.ButtonOptions{Variant: mujica.Secondary}).Clicked() {
d.price -= 12.5
log(fmt.Sprintf("price %.2f", d.price))
}
})
mujica.PriceText(c, d.price, mujica.PriceTextOptions{Size: 28, Compare: true, Reference: 18234.5, NoFlash: d.noFlash, NoGrouping: !d.grouping})
formStates(c, func() {
mujica.PriceText(c, 0.004521, mujica.PriceTextOptions{Decimals: 6})
mujica.PriceText(c, 100, mujica.PriceTextOptions{Compare: true, Reference: 100})
})
}
type tradingBadgeState struct {
redUp, hidePercent bool
change float64
}
func tradingBadgeDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "164", func() tradingBadgeState { return tradingBadgeState{change: 1.25} })
tradingTrend(c, &d.redUp, log)
formToggles(c, []string{"Hide percent"}, &d.hidePercent)
if mujica.Slider(c, &d.change, mujica.SliderOptions{Min: -5, Max: 5, Step: 0.05, Label: "Change", ShowValue: true}).Changed() {
log(fmt.Sprintf("change %.2f", d.change))
}
mujica.PriceChangeBadge(c, d.change, d.change/1.47, mujica.PriceChangeBadgeOptions{HidePercent: d.hidePercent})
formStates(c, func() {
mujica.PriceChangeBadge(c, 2.4, 1.2, mujica.PriceChangeBadgeOptions{})
mujica.PriceChangeBadge(c, -2.4, -1.2, mujica.PriceChangeBadgeOptions{})
mujica.PriceChangeBadge(c, 0, 0, mujica.PriceChangeBadgeOptions{})
})
}
type tradingBookState struct {
redUp, disabled bool
step int
book mujica.OrderBookState
}
func tradingBookDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "165", func() tradingBookState { return tradingBookState{} })
tradingTrend(c, &d.redUp, log)
formToggles(c, []string{"Disabled"}, &d.disabled)
if tradingAction(c, "Next update") {
d.step++
log(fmt.Sprintf("book update %d", d.step))
}
ui.Box(c).Width(320).Children(func() {
r := mujica.OrderBook(c, &d.book, tradingBook(100, d.step), mujica.OrderBookOptions{Depth: 6, Groups: []float64{0.01, 0.05, 0.1}, Disabled: d.disabled})
if price, side, ok := r.Clicked(); ok {
log(fmt.Sprintf("level %v at %.2f", side, price))
}
})
}
type tradingLevel2State struct {
redUp bool
step int
}
func tradingLevel2Demo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "166", func() tradingLevel2State { return tradingLevel2State{} })
tradingTrend(c, &d.redUp, log)
if tradingAction(c, "Next update") {
d.step++
log(fmt.Sprintf("level 2 update %d", d.step))
}
book := tradingBook(100, d.step)
var data mujica.Level2Data
for i := range 5 {
data.Bids = append(data.Bids, mujica.L2Level{Price: book.Bids[i].Price, Size: book.Bids[i].Size, Orders: int(book.Bids[i].Size / 10)})
data.Asks = append(data.Asks, mujica.L2Level{Price: book.Asks[i].Price, Size: book.Asks[i].Size, Orders: int(book.Asks[i].Size / 10)})
}
ui.Box(c).Width(620).Children(func() { mujica.Level2Quotes(c, data, mujica.Level2QuotesOptions{}) })
}
type tradingTapeState struct {
redUp bool
trades []mujica.TradePrint
tape mujica.TimeAndSalesState
}
func tradingTapeDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "167", func() tradingTapeState { return tradingTapeState{} })
tradingTrend(c, &d.redUp, log)
if tradingAction(c, "Add 20 trades") {
at := time.Date(2026, 10, 5, 9, 30, 0, 0, time.Local)
for range 20 {
n := len(d.trades)
side := mujica.SideBuy
if n%3 == 1 {
side = mujica.SideSell
}
d.trades = append(d.trades, mujica.TradePrint{ID: fmt.Sprint(n), Time: at.Add(time.Duration(n) * time.Second),
Price: 100 + float64(n%17)/100, Size: float64((n*37)%1500 + 1), Side: side})
}
log(fmt.Sprintf("%d trades", len(d.trades)))
}
ui.Box(c).Width(460).Children(func() {
if mujica.TimeAndSales(c, &d.tape, d.trades, mujica.TimeAndSalesOptions{LargeSize: 1000}).Changed() {
log(fmt.Sprintf("paused %v", d.tape.Paused))
}
})
}
type tradingLadderState struct {
redUp, disabled bool
last float64
ladder mujica.DepthLadderState
}
func tradingLadderDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "168", func() tradingLadderState { return tradingLadderState{last: 100} })
tradingTrend(c, &d.redUp, log)
formToggles(c, []string{"Disabled"}, &d.disabled)
if mujica.Slider(c, &d.last, mujica.SliderOptions{Min: 99.9, Max: 100.1, Step: 0.01, Label: "Last price", ShowValue: true}).Changed() {
log(fmt.Sprintf("last %.2f", d.last))
}
ui.Box(c).Width(360).Children(func() {
r := mujica.DepthLadder(c, &d.ladder, d.last, tradingBook(100, 0), mujica.DepthLadderOptions{TickSize: 0.01, Rows: 11, Disabled: d.disabled})
if price, side, ok := r.Clicked(); ok {
log(fmt.Sprintf("order %v at %.2f", side, price))
}
})
}
+156
View File
@@ -0,0 +1,156 @@
package main
// tradingCode holds each page's copyable snippet, the same as its doc and Example.
var tradingCode = map[string]string{
"TickerTape": `items := []mujica.TickerItem{{Symbol: "MUJC", Price: 412.08, ChangePercent: -1.35}, {Symbol: "AVE", Price: 88.2, ChangePercent: 2.1}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.TickerTape(c, items, mujica.TickerTapeOptions{})
}
ui.Render(view, 400, 300, 1)`,
"QuoteCard": `quote := mujica.Quote{Symbol: "MUJC", Name: "Ave Mujica Holdings", Price: 412.08, Change: -5.62, ChangePercent: -1.35}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.QuoteCard(c, quote, mujica.QuoteCardOptions{})
}
ui.Render(view, 400, 300, 1)`,
"PriceText": `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.UseTrend(c, mujica.RedUp)
mujica.PriceText(c, 18234.5, mujica.PriceTextOptions{Compare: true, Reference: 18000})
}
ui.Render(view, 400, 300, 1)`,
"PriceChangeBadge": `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PriceChangeBadge(c, -5.62, -1.35, mujica.PriceChangeBadgeOptions{})
}
ui.Render(view, 400, 300, 1)`,
"OrderBook": `var state mujica.OrderBookState
book := mujica.OrderBookData{
Bids: []mujica.BookLevel{{Price: 100.00, Size: 5}, {Price: 99.99, Size: 3}},
Asks: []mujica.BookLevel{{Price: 100.01, Size: 1}, {Price: 100.02, Size: 2}},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if price, side, ok := mujica.OrderBook(c, &state, book, mujica.OrderBookOptions{Groups: []float64{0.01, 0.05}}).Clicked(); ok {
fmt.Println("order at", price, side)
}
}
ui.Render(view, 400, 300, 1)`,
"Level2Quotes": `data := mujica.Level2Data{
Bids: []mujica.L2Level{{Price: 100.00, Size: 500, Orders: 12}},
Asks: []mujica.L2Level{{Price: 100.01, Size: 200, Orders: 3}},
}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.Level2Quotes(c, data, mujica.Level2QuotesOptions{})
}
ui.Render(view, 400, 300, 1)`,
"TimeAndSales": `var state mujica.TimeAndSalesState
trades := []mujica.TradePrint{{ID: "1", Time: time.Now(), Price: 100.01, Size: 300, Side: mujica.SideBuy}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.TimeAndSales(c, &state, trades, mujica.TimeAndSalesOptions{LargeSize: 1000})
}
ui.Render(view, 400, 300, 1)`,
"DepthLadder": `var state mujica.DepthLadderState
book := mujica.OrderBookData{Bids: []mujica.BookLevel{{Price: 100, Size: 5}}, Asks: []mujica.BookLevel{{Price: 100.01, Size: 2}}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if price, side, ok := mujica.DepthLadder(c, &state, 100, book, mujica.DepthLadderOptions{TickSize: 0.01}).Clicked(); ok {
fmt.Println("place", side, "at", price)
}
}
ui.Render(view, 400, 300, 1)`,
"SpreadIndicator": `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.SpreadIndicator(c, 99.95, 100.05, mujica.SpreadIndicatorOptions{WarnPercent: 0.5})
}
ui.Render(view, 400, 300, 1)`,
"BidAskBar": `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.BidAskBar(c, 6234, 3820, mujica.BidAskBarOptions{})
}
ui.Render(view, 400, 300, 1)`,
"OrderEntry": `var state mujica.OrderEntryState
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if order, ok := mujica.OrderEntry(c, &state, mujica.OrderEntryOptions{Symbol: "MUJC", Last: 100.2}).Submitted(); ok {
fmt.Println("place", order.Quantity)
state.EndSubmit("")
}
}
ui.Render(view, 400, 300, 1)`,
"QuickTradeButtons": `quantity := 10.0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if side, q, ok := mujica.QuickTradeButtons(c, &quantity, mujica.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: true}).Traded(); ok {
fmt.Println(side, q)
}
}
ui.Render(view, 400, 300, 1)`,
"PositionTable": `var state mujica.DataTableState[string]
rows := []mujica.PositionRow{{ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if id, ok := mujica.PositionTable(c, &state, rows, mujica.PositionTableOptions{}).Acted(); ok {
fmt.Println("close", id)
}
}
ui.Render(view, 400, 300, 1)`,
"OrderTable": `var state mujica.OrderTableState
rows := []mujica.OrderRow{{ID: "o1", Symbol: "MUJC", Side: mujica.SideBuy, Price: 99.5, Quantity: 100, Filled: 40, Status: mujica.OrderPartial}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if id, ok := mujica.OrderTable(c, &state, rows, mujica.OrderTableOptions{}).Acted(); ok {
fmt.Println("cancel", id)
}
}
ui.Render(view, 400, 300, 1)`,
"TradeHistoryTable": `var state mujica.TradeHistoryTableState
rows := []mujica.FillRow{{ID: "f1", Time: time.Now(), Symbol: "MUJC", Side: mujica.SideBuy, Price: 400, Quantity: 10, Fee: 1.2}}
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if fills, ok := mujica.TradeHistoryTable(c, &state, rows, mujica.TradeHistoryTableOptions{}).Exported(); ok {
fmt.Println("export", len(fills))
}
}
ui.Render(view, 400, 300, 1)`,
"PnLDisplay": `hidden := false
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.PnLDisplay(c, mujica.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &hidden, mujica.PnLDisplayOptions{Currency: "$"})
}
ui.Render(view, 400, 300, 1)`,
"LeverageSlider": `leverage := 5.0
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
if mujica.LeverageSlider(c, &leverage, mujica.LeverageSliderOptions{Warning: 10, Danger: 50}).Changed() {
fmt.Println("leverage", leverage)
}
}
ui.Render(view, 400, 300, 1)`,
"MarginIndicator": `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.MarginIndicator(c, 0.86, mujica.MarginIndicatorOptions{})
}
ui.Render(view, 400, 300, 1)`,
"RiskMeter": `view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
mujica.RiskMeter(c, 4, mujica.RiskMeterOptions{})
}
ui.Render(view, 400, 300, 1)`,
"OrderConfirmDialog": `open := true
view := func(c *ui.Context) {
mujica.Use(c, mujica.Settings{})
r := mujica.OrderConfirmDialog(c, &open, mujica.OrderConfirmDialogOptions{
Lines: []mujica.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Quantity", Value: "100"}},
Risk: "Leveraged products can lose more than the deposit.",
Acknowledge: "I understand the risk",
})
if r.Confirmed {
fmt.Println("send order")
}
}
ui.Render(view, 400, 300, 1)`,
}
+286
View File
@@ -0,0 +1,286 @@
package main
import (
"fmt"
"time"
"github.com/egoist/mygo/ui"
mujica "mujicaui"
)
type tradingSpreadState struct {
ask, warn float64
}
func tradingSpreadDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "169", func() tradingSpreadState { return tradingSpreadState{ask: 100.05, warn: 0.5} })
if mujica.Slider(c, &d.ask, mujica.SliderOptions{Min: 99.5, Max: 101, Step: 0.01, Label: "Ask (bid 99.95)", ShowValue: true}).Changed() {
log(fmt.Sprintf("ask %.2f", d.ask))
}
if mujica.Slider(c, &d.warn, mujica.SliderOptions{Min: 0, Max: 2, Step: 0.1, Label: "Warn at percent", ShowValue: true}).Changed() {
log(fmt.Sprintf("warn %.1f%%", d.warn))
}
mujica.SpreadIndicator(c, 99.95, d.ask, mujica.SpreadIndicatorOptions{WarnPercent: d.warn})
formStates(c, func() {
mujica.SpreadIndicator(c, 99.95, 100.05, mujica.SpreadIndicatorOptions{})
mujica.SpreadIndicator(c, 99, 101, mujica.SpreadIndicatorOptions{WarnPercent: 0.5})
mujica.SpreadIndicator(c, 100.1, 100, mujica.SpreadIndicatorOptions{})
})
}
type tradingBarState struct {
redUp bool
bid, ask float64
}
func tradingBarDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "170", func() tradingBarState { return tradingBarState{bid: 6234, ask: 3820} })
tradingTrend(c, &d.redUp, log)
for _, v := range []struct {
name string
p *float64
}{{"Bid volume", &d.bid}, {"Ask volume", &d.ask}} {
if mujica.Slider(c, v.p, mujica.SliderOptions{Min: 0, Max: 10000, Step: 1, Label: v.name, ShowValue: true}).Changed() {
log(fmt.Sprintf("%s %.0f", v.name, *v.p))
}
}
ui.Box(c).Width(360).Children(func() { mujica.BidAskBar(c, d.bid, d.ask, mujica.BidAskBarOptions{}) })
}
type tradingEntryState struct {
redUp, disabled, reject bool
entry mujica.OrderEntryState
}
func tradingEntryDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "171", func() tradingEntryState { return tradingEntryState{} })
checks := *ui.Local(c.Root(), "171.checks", func() chan [2]any { return make(chan [2]any, 8) })
placed := *ui.Local(c.Root(), "171.placed", func() chan string { return make(chan string, 1) })
for drained := false; !drained; {
select {
case r := <-checks:
v, msg := r[0].(uint64), r[1].(string)
log(fmt.Sprintf("buying power check v%d: %q, kept %v", v, msg, d.entry.Resolve(v, msg)))
case e := <-placed:
d.entry.EndSubmit(e)
log(fmt.Sprintf("order placed, error %q", e))
default:
drained = true
}
}
tradingTrend(c, &d.redUp, log)
formToggles(c, []string{"Disabled", "Broker rejects"}, &d.disabled, &d.reject)
r := mujica.OrderEntry(c, &d.entry, mujica.OrderEntryOptions{Symbol: "MUJC", Last: 100.2, MaxQuantity: 5000, Disabled: d.disabled,
Check: func(o mujica.OrderDraft, v uint64) {
log(fmt.Sprintf("check started v%d", v))
go func() {
time.Sleep(600 * time.Millisecond)
msg := ""
if o.Quantity*100 > 250000 {
msg = "Not enough buying power."
}
checks <- [2]any{v, msg}
c.Invalidate()
}()
}})
if o, ok := r.Submitted(); ok {
log(fmt.Sprintf("submitted %+v", o))
fail := ""
if d.reject {
fail = "The broker rejected the order. Your entry is kept."
}
go func() {
time.Sleep(800 * time.Millisecond)
placed <- fail
c.Invalidate()
}()
}
}
type tradingQuickState struct {
redUp, confirm, disabled bool
qty float64
}
func tradingQuickDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "172", func() tradingQuickState { return tradingQuickState{confirm: true, qty: 10} })
tradingTrend(c, &d.redUp, log)
formToggles(c, []string{"Confirm first", "Disabled"}, &d.confirm, &d.disabled)
ui.Box(c).Width(400).Children(func() {
if side, q, ok := mujica.QuickTradeButtons(c, &d.qty, mujica.QuickTradeButtonsOptions{Symbol: "MUJC", Confirm: d.confirm, Disabled: d.disabled}).Traded(); ok {
log(fmt.Sprintf("trade %v %v", side, q))
}
})
}
type tradingPositionsState struct {
redUp, disabled bool
rows []mujica.PositionRow
table mujica.DataTableState[string]
}
func tradingPositionsDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "173", func() tradingPositionsState {
return tradingPositionsState{rows: []mujica.PositionRow{
{ID: "p1", Symbol: "MUJC", Quantity: 100, AvgCost: 400, Price: 412.08},
{ID: "p2", Symbol: "AVE", Quantity: 50, AvgCost: 90, Price: 88.2},
{ID: "p3", Symbol: "OBLV", Quantity: 8, AvgCost: 1180, Price: 1204.5},
}}
})
tradingTrend(c, &d.redUp, log)
formToggles(c, []string{"Disabled"}, &d.disabled)
ui.Box(c).Width(1000).Height(260).Children(func() {
r := mujica.PositionTable(c, &d.table, d.rows, mujica.PositionTableOptions{Disabled: d.disabled})
if id, ok := r.Acted(); ok {
for i, p := range d.rows {
if p.ID == id {
d.rows = append(d.rows[:i:i], d.rows[i+1:]...)
break
}
}
log("close " + id)
}
if r.Table.Changed() {
id, _ := d.table.Selected()
log("select " + id)
}
})
}
type tradingOrdersState struct {
disabled bool
rows []mujica.OrderRow
table mujica.OrderTableState
version int
}
func tradingOrdersDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "174", func() tradingOrdersState {
return tradingOrdersState{rows: []mujica.OrderRow{
{ID: "o1", Symbol: "MUJC", Side: mujica.SideBuy, Type: mujica.OrderLimit, Price: 99.5, Quantity: 100, Filled: 40, Status: mujica.OrderPartial},
{ID: "o2", Symbol: "AVE", Side: mujica.SideSell, Type: mujica.OrderMarket, Quantity: 10, Filled: 10, Status: mujica.OrderFilled},
{ID: "o3", Symbol: "OBLV", Side: mujica.SideBuy, Type: mujica.OrderStop, Price: 12, Quantity: 5, Status: mujica.OrderCanceled},
{ID: "o4", Symbol: "DOLO", Side: mujica.SideSell, Type: mujica.OrderLimit, Price: 7.25, Quantity: 30, Status: mujica.OrderOpen},
}}
})
formToggles(c, []string{"Disabled"}, &d.disabled)
ui.Box(c).Width(1000).Height(300).Children(func() {
if id, ok := mujica.OrderTable(c, &d.table, d.rows, mujica.OrderTableOptions{Disabled: d.disabled, Version: d.version}).Acted(); ok {
for i := range d.rows {
if d.rows[i].ID == id {
d.rows[i].Status = mujica.OrderCanceled
}
}
d.version++
log("cancel " + id)
}
})
}
type tradingHistoryState struct {
disabled bool
rows []mujica.FillRow
table mujica.TradeHistoryTableState
}
func tradingHistoryDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "175", func() tradingHistoryState {
var rows []mujica.FillRow
for i := range 30 {
side := mujica.SideBuy
if i%3 == 0 {
side = mujica.SideSell
}
rows = append(rows, mujica.FillRow{ID: fmt.Sprint(i), Time: time.Date(2026, 9, 10+i/2, 9+i%6, 0, 0, 0, time.Local),
Symbol: []string{"MUJC", "AVE", "OBLV"}[i%3], Side: side, Price: 100 + float64(i), Quantity: float64(i%5+1) * 10, Fee: 0.5})
}
return tradingHistoryState{rows: rows}
})
formToggles(c, []string{"Disabled"}, &d.disabled)
ui.Box(c).Width(1000).Height(360).Children(func() {
if fills, ok := mujica.TradeHistoryTable(c, &d.table, d.rows, mujica.TradeHistoryTableOptions{Disabled: d.disabled}).Exported(); ok {
log(fmt.Sprintf("export %d fills", len(fills)))
}
})
}
type tradingPnLState struct {
redUp, hidden bool
}
func tradingPnLDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "176", func() tradingPnLState { return tradingPnLState{} })
tradingTrend(c, &d.redUp, log)
if mujica.PnLDisplay(c, mujica.PnL{Realized: 1200.5, Unrealized: -340.2, Today: 88}, &d.hidden, mujica.PnLDisplayOptions{Currency: "$"}).Changed() {
log(fmt.Sprintf("hidden %v", d.hidden))
}
}
type tradingLeverageState struct {
disabled bool
value float64
}
func tradingLeverageDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "177", func() tradingLeverageState { return tradingLeverageState{value: 5} })
formToggles(c, []string{"Disabled"}, &d.disabled)
ui.Box(c).Width(380).Children(func() {
if mujica.LeverageSlider(c, &d.value, mujica.LeverageSliderOptions{Warning: 10, Danger: 50, Disabled: d.disabled}).Changed() {
log(fmt.Sprintf("leverage applied %v×", d.value))
}
})
}
type tradingMarginState struct{ ratio float64 }
func tradingMarginDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "178", func() tradingMarginState { return tradingMarginState{ratio: 0.55} })
if mujica.Slider(c, &d.ratio, mujica.SliderOptions{Min: 0, Max: 1.4, Step: 0.01, Label: "Margin ratio", ShowValue: true}).Changed() {
log(fmt.Sprintf("ratio %.2f", d.ratio))
}
ui.Box(c).Width(360).Children(func() { mujica.MarginIndicator(c, d.ratio, mujica.MarginIndicatorOptions{}) })
formStates(c, func() {
for _, r := range []float64{0.3, 0.85, 1.1} {
ui.Box(c).Width(260).Children(func() { mujica.MarginIndicator(c, r, mujica.MarginIndicatorOptions{}) })
}
})
}
type tradingRiskState struct{ level float64 }
func tradingRiskDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "179", func() tradingRiskState { return tradingRiskState{level: 3} })
if mujica.Slider(c, &d.level, mujica.SliderOptions{Min: 1, Max: 5, Step: 1, Label: "Level", ShowValue: true}).Changed() {
log(fmt.Sprintf("level %.0f", d.level))
}
ui.Box(c).Width(360).Children(func() { mujica.RiskMeter(c, int(d.level), mujica.RiskMeterOptions{}) })
}
type tradingConfirmState struct {
open, acknowledge bool
}
func tradingConfirmDemo(c *ui.Context, log func(string)) {
d := ui.Local(c.Root(), "180", func() tradingConfirmState { return tradingConfirmState{acknowledge: true} })
formToggles(c, []string{"Require acknowledgement"}, &d.acknowledge)
if tradingAction(c, "Review order") {
d.open = true
log("open")
}
ack := ""
if d.acknowledge {
ack = "I understand leveraged products can lose more than the deposit"
}
r := mujica.OrderConfirmDialog(c, &d.open, mujica.OrderConfirmDialogOptions{
Lines: []mujica.OrderSummaryLine{{Label: "Symbol", Value: "MUJC"}, {Label: "Side", Value: "Buy"}, {Label: "Type", Value: "Limit 100.20"},
{Label: "Quantity", Value: "100"}, {Label: "Estimated amount", Value: "10,020.00"}},
Risk: "5× leverage: a 20% move against you wipes out the margin.", Acknowledge: ack,
})
if r.Confirmed {
log("confirmed")
}
if r.Canceled {
log("canceled")
}
}