Merge branch 'main' into worktree-agent-ab9ba373bc47c2352
This commit is contained in:
1005 files changed
+62360
-18
No files matched your search
@@ -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"}
|
||||
@@ -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, ¤t, 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, ¤t, 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)`
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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."},
|
||||
}
|
||||
@@ -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)`
|
||||
)
|
||||
@@ -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:]...)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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())))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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)`
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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}))
|
||||
})
|
||||
}
|
||||
@@ -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}))
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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})
|
||||
})
|
||||
}},
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
})
|
||||
}},
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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)`},
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)`
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
},
|
||||
}
|
||||
@@ -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})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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])
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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] }})
|
||||
})
|
||||
}
|
||||
@@ -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, ¬es, 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, ¤t, 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)`
|
||||
)
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -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)`,
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user