feat: add instance form visual boundaries

This commit is contained in:
QiuSW
2026-07-27 09:40:37 +08:00
parent 79630e0d48
commit 57ad4b899b
5 changed files with 130 additions and 50 deletions
+89 -46
View File
@@ -41,10 +41,13 @@ const (
)
var (
instanceStartIcon = mustIcon(icons.AVPlayArrow)
instanceStopIcon = mustIcon(icons.AVStop)
instanceEditIcon = mustIcon(icons.EditorModeEdit)
instanceDeleteIcon = mustIcon(icons.ActionDelete)
instanceStartIcon = mustIcon(icons.AVPlayArrow)
instanceStopIcon = mustIcon(icons.AVStop)
instanceEditIcon = mustIcon(icons.EditorModeEdit)
instanceDeleteIcon = mustIcon(icons.ActionDelete)
instanceFormWorkspaceBorder = color.NRGBA{R: 199, G: 207, B: 218, A: 255}
instanceFormComponentBorder = color.NRGBA{R: 225, G: 230, B: 238, A: 255}
instanceFormActionBorder = color.NRGBA{R: 215, G: 222, B: 232, A: 255}
)
type PathField string
@@ -1114,8 +1117,14 @@ func (s *Shell) editDialogCard(row InstanceRow) layout.Widget {
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
rect := image.Rectangle{Max: gtx.Constraints.Min}
defer clip.UniformRRect(rect, gtx.Dp(8)).Push(gtx.Ops).Pop()
paint.Fill(gtx.Ops, s.theme.Palette.Bg)
if rect.Empty() {
return layout.Dimensions{Size: rect.Max}
}
paint.FillShape(gtx.Ops, instanceFormWorkspaceBorder, clip.UniformRRect(rect, gtx.Dp(10)).Op(gtx.Ops))
inner := rect.Inset(gtx.Dp(1))
if !inner.Empty() {
paint.FillShape(gtx.Ops, s.theme.Palette.Bg, clip.UniformRRect(inner, gtx.Dp(9)).Op(gtx.Ops))
}
return layout.Dimensions{Size: gtx.Constraints.Min}
},
func(gtx layout.Context) layout.Dimensions {
@@ -1126,25 +1135,29 @@ func (s *Shell) editDialogCard(row InstanceRow) layout.Widget {
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(material.Caption(s.theme, "修改下次启动时使用的保存配置。").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
layout.Rigid(s.formField("实例名称", "用于在实例列表中识别此浏览器环境", &s.editName)),
layout.Rigid(s.instanceFormField(s.formField("实例名称", "用于在实例列表中识别此浏览器环境", &s.editName))),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.editBrowserField(gtx, locked) }),
layout.Rigid(s.instanceFormField(func(gtx layout.Context) layout.Dimensions { return s.editBrowserField(gtx, locked) })),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.editDirectoryField(gtx, locked) }),
layout.Rigid(s.instanceFormField(func(gtx layout.Context) layout.Dimensions { return s.editDirectoryField(gtx, locked) })),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.editURL)),
layout.Rigid(s.instanceFormField(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.editURL))),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.editPreferredPortField(gtx, locked) }),
layout.Rigid(s.instanceFormField(func(gtx layout.Context) layout.Dimensions { return s.editPreferredPortField(gtx, locked) })),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerEdit) }),
layout.Rigid(s.instanceFormField(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerEdit) })),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.formField("实际调试端口", "仅在已验证的运行或外部关联状态显示;此处只读", &s.editPort)),
layout.Rigid(s.instanceFormField(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(s.formField("实际调试端口", "仅在已验证的运行或外部关联状态显示;此处只读", &s.editPort)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(statusLabel(s.theme, row.Status)),
)
})),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(statusLabel(s.theme, row.Status)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(s.editFeedbackLabel(locked)),
layout.Rigid(s.instanceFormFeedback(s.editFeedbackMessage(locked))),
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
layout.Rigid(s.editActions),
layout.Rigid(s.instanceFormActionBar(s.editActions)),
)
if s.editFocusPending {
gtx.Execute(key.FocusCmd{Tag: &s.editName})
@@ -1208,12 +1221,12 @@ func (s *Shell) editPreferredPortField(gtx layout.Context, locked bool) layout.D
)
}
func (s *Shell) editFeedbackLabel(locked bool) layout.Widget {
func (s *Shell) editFeedbackMessage(locked bool) string {
label := s.editFeedback
if label == "" && locked {
label = "运行中或外部关联实例的浏览器类型和 User Data Dir 已锁定。"
}
return material.Caption(s.theme, label).Layout
return label
}
func (s *Shell) editActions(gtx layout.Context) layout.Dimensions {
@@ -1727,34 +1740,31 @@ func (s *Shell) createInstance(gtx layout.Context) layout.Dimensions {
layout.Rigid(material.H4(s.theme, "新建实例").Layout),
layout.Rigid(material.Body1(s.theme, "为一个独立的 Chrome 或 Edge User Data Dir 创建托管实例").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout),
layout.Rigid(s.formField("实例名称", "用于在实例列表中识别此浏览器环境", &s.instanceName)),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.instanceDirField),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.instanceURL)),
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
layout.Rigid(s.formField("首选调试端口", "已按现有实例推荐;启动时会在后台复检 loopback 可用性", &s.instancePort)),
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerCreate) }),
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
layout.Rigid(material.Body2(s.theme, "浏览器类型").Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(material.RadioButton(s.theme, &s.browserKind, "chrome", "Chrome").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(material.RadioButton(s.theme, &s.browserKind, "edge", "Edge").Layout),
layout.Rigid(s.instanceFormWorkspace(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(s.instanceFormField(s.formField("实例名称", "用于在实例列表中识别此浏览器环境", &s.instanceName))),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.instanceFormField(s.instanceDirField)),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.instanceFormField(s.createBrowserField)),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.instanceFormField(s.formField("启动 URL(可选)", "仅支持 http/https;留空时启动浏览器默认页", &s.instanceURL))),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.instanceFormField(s.formField("首选调试端口", "已按现有实例推荐;启动时会在后台复检 loopback 可用性", &s.instancePort))),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.instanceFormField(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerCreate) })),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.instanceFormFeedback(s.formFeedback)),
layout.Rigid(layout.Spacer{Height: unit.Dp(12)}.Layout),
layout.Rigid(s.instanceFormActionBar(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(material.Button(s.theme, &s.createClick, "创建实例").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(material.Button(s.theme, &s.backClick, "取消").Layout),
)
})),
)
}),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(material.Caption(s.theme, s.formFeedback).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(material.Button(s.theme, &s.createClick, "创建实例").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(material.Button(s.theme, &s.backClick, "取消").Layout),
)
}),
})),
)
}
@@ -1769,6 +1779,39 @@ func (s *Shell) formField(label, help string, editor *widget.Editor) layout.Widg
}
}
func (s *Shell) createBrowserField(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Body2(s.theme, "浏览器类型").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(material.RadioButton(s.theme, &s.browserKind, "chrome", "Chrome").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(material.RadioButton(s.theme, &s.browserKind, "edge", "Edge").Layout),
)
}),
)
}
func (s *Shell) instanceFormWorkspace(content layout.Widget) layout.Widget {
return s.settingsSurface(instanceFormWorkspaceBorder, unit.Dp(10), unit.Dp(12), content)
}
func (s *Shell) instanceFormField(content layout.Widget) layout.Widget {
return s.settingsSurface(instanceFormComponentBorder, unit.Dp(6), unit.Dp(8), content)
}
func (s *Shell) instanceFormActionBar(content layout.Widget) layout.Widget {
return s.settingsSurface(instanceFormActionBorder, unit.Dp(6), unit.Dp(8), content)
}
func (s *Shell) instanceFormFeedback(message string) layout.Widget {
if strings.TrimSpace(message) == "" {
return func(layout.Context) layout.Dimensions { return layout.Dimensions{} }
}
return s.settingsSurface(instanceFormActionBorder, unit.Dp(6), unit.Dp(8), material.Caption(s.theme, message).Layout)
}
func (s *Shell) proxyPickerField(gtx layout.Context, target proxyPickerTarget) layout.Dimensions {
click := &s.createProxyPick
selected := s.createProxyID