feat: add compact instance actions

This commit is contained in:
QiuSW
2026-07-22 16:37:34 +08:00
parent 7fcc9752a1
commit 1e5565e440
2 changed files with 103 additions and 16 deletions
+68 -12
View File
@@ -10,6 +10,7 @@ import (
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"golang.org/x/exp/shiny/materialdesign/icons"
)
type page uint8
@@ -21,11 +22,16 @@ const (
)
const (
instanceNameColumnWeight = 25
instanceNameColumnWeight = 24
instanceBrowserColumnWeight = 12
instanceDirectoryColumnWeight = 39
instanceStatusColumnWeight = 14
instanceStartColumnWeight = 10
instanceDirectoryColumnWeight = 38
instanceStatusColumnWeight = 12
instanceActionColumnWeight = 14
)
var (
instanceStartIcon = mustIcon(icons.AVPlayArrow)
instanceDeleteIcon = mustIcon(icons.ActionDelete)
)
type PathField string
@@ -123,6 +129,7 @@ type Shell struct {
list widget.List
rows []InstanceRow
startClicks map[string]*widget.Clickable
deleteClicks map[string]*widget.Clickable
nextInstance uint64
instanceFeedback string
onSave func(SettingsState)
@@ -146,7 +153,7 @@ func NewShell(theme *material.Theme) *Shell {
PathEdgeExecutable: {},
PathDefaultUserData: {},
PathLogDirectory: {},
}, startClicks: make(map[string]*widget.Clickable), nextInstance: 4, searchResults: make(chan pathSearchResult, 8), directoryResults: make(chan directoryPickResult, 1)}
}, startClicks: make(map[string]*widget.Clickable), deleteClicks: make(map[string]*widget.Clickable), nextInstance: 4, searchResults: make(chan pathSearchResult, 8), directoryResults: make(chan directoryPickResult, 1)}
s.chromePath.SetText(`C:\Program Files\Google\Chrome\Application\chrome.exe`)
s.edgePath.SetText(`C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe`)
s.dataDir.SetText(`C:\Users\Public\chub\profiles`)
@@ -222,6 +229,11 @@ func (s *Shell) Layout(gtx layout.Context) layout.Dimensions {
s.requestStart(row.ID)
}
}
for _, row := range append([]InstanceRow(nil), s.rows...) {
for s.deleteClickFor(row.ID).Clicked(gtx) {
s.deleteInstance(row.ID)
}
}
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Rigid(s.sidebar),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
@@ -279,7 +291,7 @@ func (s *Shell) instances(gtx layout.Context) layout.Dimensions {
layout.Flexed(instanceBrowserColumnWeight, material.Body2(s.theme, row.Browser).Layout),
layout.Flexed(instanceDirectoryColumnWeight, pathCell(s.theme, row.UserDataDir)),
layout.Flexed(instanceStatusColumnWeight, statusLabel(s.theme, row.Status)),
layout.Flexed(instanceStartColumnWeight, s.instanceStartButton(s.startClickFor(row.ID))),
layout.Flexed(instanceActionColumnWeight, s.instanceActionButtons(row)),
)
})
})
@@ -296,8 +308,8 @@ func (s *Shell) instanceHeader(gtx layout.Context) layout.Dimensions {
layout.Flexed(instanceBrowserColumnWeight, material.Caption(s.theme, "浏览器类型").Layout),
layout.Flexed(instanceDirectoryColumnWeight, material.Caption(s.theme, "用户数据目录").Layout),
layout.Flexed(instanceStatusColumnWeight, material.Caption(s.theme, "状态").Layout),
layout.Flexed(instanceStartColumnWeight, func(gtx layout.Context) layout.Dimensions {
return layout.E.Layout(gtx, material.Caption(s.theme, "启动").Layout)
layout.Flexed(instanceActionColumnWeight, func(gtx layout.Context) layout.Dimensions {
return layout.E.Layout(gtx, material.Caption(s.theme, "操作").Layout)
}),
)
})
@@ -309,16 +321,29 @@ func pathCell(theme *material.Theme, path string) layout.Widget {
return style.Layout
}
func (s *Shell) instanceStartButton(click *widget.Clickable) layout.Widget {
func (s *Shell) instanceActionButtons(row InstanceRow) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
return layout.E.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
style := material.Button(s.theme, click, "启动")
style.Inset = layout.Inset{Top: unit.Dp(6), Right: unit.Dp(10), Bottom: unit.Dp(6), Left: unit.Dp(10)}
return style.Layout(gtx)
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(s.instanceIconButton(s.startClickFor(row.ID), instanceStartIcon, "启动 "+row.Name, s.theme.Palette.ContrastBg)),
layout.Rigid(layout.Spacer{Width: unit.Dp(4)}.Layout),
layout.Rigid(s.instanceIconButton(s.deleteClickFor(row.ID), instanceDeleteIcon, "删除 "+row.Name, color.NRGBA{R: 188, G: 51, B: 51, A: 255})),
)
})
}
}
func (s *Shell) instanceIconButton(click *widget.Clickable, icon *widget.Icon, description string, iconColor color.NRGBA) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
style := material.IconButton(s.theme, click, icon, description)
style.Size = unit.Dp(18)
style.Inset = layout.UniformInset(unit.Dp(7))
style.Background = color.NRGBA{}
style.Color = iconColor
return style.Layout(gtx)
}
}
func statusLabel(theme *material.Theme, status string) layout.Widget {
style := material.Label(theme, unit.Sp(13), status)
style.Color = map[string]color.NRGBA{
@@ -574,6 +599,15 @@ func (s *Shell) startClickFor(id string) *widget.Clickable {
return click
}
func (s *Shell) deleteClickFor(id string) *widget.Clickable {
if click := s.deleteClicks[id]; click != nil {
return click
}
click := new(widget.Clickable)
s.deleteClicks[id] = click
return click
}
func (s *Shell) requestStart(id string) {
for _, row := range s.rows {
if row.ID != id {
@@ -585,6 +619,28 @@ func (s *Shell) requestStart(id string) {
s.instanceFeedback = "找不到要启动的实例。"
}
func (s *Shell) deleteInstance(id string) {
for i, row := range s.rows {
if row.ID != id {
continue
}
s.rows = append(s.rows[:i], s.rows[i+1:]...)
delete(s.startClicks, id)
delete(s.deleteClicks, id)
s.instanceFeedback = fmt.Sprintf("已删除实例“%s”;其 User Data Dir 未被删除。", row.Name)
return
}
s.instanceFeedback = "找不到要删除的实例。"
}
func mustIcon(data []byte) *widget.Icon {
icon, err := widget.NewIcon(data)
if err != nil {
panic(err)
}
return icon
}
func (s *Shell) chooseInstanceDirectory() {
if s.directoryPick.running {
s.formFeedback = "目录选择器已打开,请在系统窗口中选择或取消。"
+35 -4
View File
@@ -21,7 +21,7 @@ func TestShellStartsWithSemanticInstanceStatuses(t *testing.T) {
}
}
func TestShellInstanceRowsContainRequiredColumnsAndIndependentStartControls(t *testing.T) {
func TestShellInstanceRowsContainRequiredColumnsAndIndependentActionControls(t *testing.T) {
shell := NewShell(material.NewTheme())
if len(shell.rows) < 2 {
t.Fatal("expected fixture rows")
@@ -36,18 +36,49 @@ func TestShellInstanceRowsContainRequiredColumnsAndIndependentStartControls(t *t
if first == second {
t.Fatal("rows share a start button state")
}
if shell.deleteClickFor(shell.rows[0].ID) == shell.deleteClickFor(shell.rows[1].ID) {
t.Fatal("rows share a delete button state")
}
}
func TestInstanceListColumnWeightsPrioritizeDirectoryAndCompactAction(t *testing.T) {
got := instanceNameColumnWeight + instanceBrowserColumnWeight + instanceDirectoryColumnWeight + instanceStatusColumnWeight + instanceStartColumnWeight
got := instanceNameColumnWeight + instanceBrowserColumnWeight + instanceDirectoryColumnWeight + instanceStatusColumnWeight + instanceActionColumnWeight
if got != 100 {
t.Fatalf("instance column weights = %d, want 100", got)
}
if instanceDirectoryColumnWeight <= instanceNameColumnWeight {
t.Fatal("user data directory must have the widest instance-list column")
}
if instanceStartColumnWeight >= instanceStatusColumnWeight {
t.Fatal("start action column must remain compact")
if instanceActionColumnWeight <= instanceStatusColumnWeight {
t.Fatal("actions column must fit both compact icon commands")
}
}
func TestShellDeletesOnlyTheRequestedInstance(t *testing.T) {
shell := NewShell(material.NewTheme())
target := shell.rows[1]
remaining := shell.rows[2].ID
shell.startClickFor(target.ID)
shell.deleteClickFor(target.ID)
shell.deleteInstance(target.ID)
if len(shell.rows) != 3 {
t.Fatalf("instance count = %d, want 3", len(shell.rows))
}
for _, row := range shell.rows {
if row.ID == target.ID {
t.Fatalf("deleted instance %q remains in the list", target.ID)
}
}
if _, exists := shell.startClicks[target.ID]; exists {
t.Fatal("deleted instance start control was not released")
}
if _, exists := shell.deleteClicks[target.ID]; exists {
t.Fatal("deleted instance delete control was not released")
}
if shell.rows[1].ID != remaining {
t.Fatalf("remaining instance id = %q, want %q", shell.rows[1].ID, remaining)
}
}