feat: add settings page visual boundaries

This commit is contained in:
QiuSW
2026-07-26 17:42:40 +08:00
parent eeaf4c7826
commit 45ea8f3194
5 changed files with 197 additions and 57 deletions
+144 -47
View File
@@ -302,6 +302,7 @@ type Shell struct {
proxyDeleteFocus bool
list widget.List
settingsList widget.List
rows []InstanceRow
selectedInstanceID string
rowClicks map[string]*widget.Clickable
@@ -372,6 +373,7 @@ func NewShell(theme *material.Theme) *Shell {
s.editBrowserKind.Value = "chrome"
s.editPort.ReadOnly = true
s.list.Axis = layout.Vertical
s.settingsList.Axis = layout.Vertical
return s
}
@@ -1476,51 +1478,125 @@ func (s *Shell) settings(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.H4(s.theme, "设置").Layout),
layout.Rigid(material.Body1(s.theme, "配置浏览器可执行文件、默认目录和本地运行行为").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(18)}.Layout),
layout.Rigid(s.pathField(PathChromeExecutable, "Chrome 可执行文件", &s.chromePath, &s.chromePick, &s.chromeSearch)),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.pathField(PathEdgeExecutable, "Edge 可执行文件", &s.edgePath, &s.edgePick, &s.edgeSearch)),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.pathField(PathDefaultUserData, "默认 User Data Dir", &s.dataDir, &s.dataPick, &s.dataSearch)),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.pathField(PathLogDirectory, "日志目录", &s.logDir, &s.logPick, &s.logSearch)),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(s.remoteDebugPortField),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(material.Caption(s.theme, s.pathFeedback).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(material.CheckBox(s.theme, &s.closeOnExit, "退出时关闭托管浏览器").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
layout.Rigid(s.proxySettings),
layout.Rigid(layout.Spacer{Height: unit.Dp(16)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(material.Button(s.theme, &s.saveClick, "保存设置").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(material.Button(s.theme, &s.cancelClick, "取消").Layout),
)
layout.Rigid(layout.Spacer{Height: unit.Dp(14)}.Layout),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
return material.List(s.theme, &s.settingsList).Layout(gtx, 1, func(gtx layout.Context, _ int) layout.Dimensions {
return layout.Inset{Bottom: unit.Dp(8)}.Layout(gtx, s.settingsWorkspace)
})
}),
)
}
func (s *Shell) settingsWorkspace(gtx layout.Context) layout.Dimensions {
return s.settingsSurface(color.NRGBA{R: 199, G: 207, B: 218, A: 255}, unit.Dp(10), unit.Dp(12), func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(s.settingsSection("浏览器路径", "分别指定 Chrome 与 Edge 的可执行文件;留空时仍可使用自动发现。", func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(s.pathField(PathChromeExecutable, "Chrome 可执行文件", &s.chromePath, &s.chromePick, &s.chromeSearch)),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.pathField(PathEdgeExecutable, "Edge 可执行文件", &s.edgePath, &s.edgePick, &s.edgeSearch)),
)
})),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.settingsSection("默认目录", "这些目录只作为新建实例和诊断日志的默认位置,不会移动或删除已有 profile。", func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(s.pathField(PathDefaultUserData, "默认 User Data Dir", &s.dataDir, &s.dataPick, &s.dataSearch)),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.pathField(PathLogDirectory, "日志目录", &s.logDir, &s.logPick, &s.logSearch)),
)
})),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.settingsSection("运行行为", "影响后续启动时的端口分配,以及退出 Chub 时的默认处理。", func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(s.remoteDebugPortField),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.settingsField(func(gtx layout.Context) layout.Dimensions {
return material.CheckBox(s.theme, &s.closeOnExit, "退出时关闭托管浏览器").Layout(gtx)
})),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.settingsFeedback(s.pathFeedback)),
)
})),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.settingsSection("代理", "仅保存无认证 scheme://host:port 端点;下拉框和实例表单显示完整地址。", s.proxySettings)),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(s.settingsActionBar(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(material.Button(s.theme, &s.saveClick, "保存设置").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(material.Button(s.theme, &s.cancelClick, "取消").Layout),
)
})),
)
})(gtx)
}
func (s *Shell) settingsSection(title, description string, content layout.Widget) layout.Widget {
return s.settingsSurface(color.NRGBA{R: 215, G: 222, B: 232, A: 255}, unit.Dp(8), unit.Dp(12), func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.H6(s.theme, title).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(2)}.Layout),
layout.Rigid(material.Caption(s.theme, description).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(10)}.Layout),
layout.Rigid(content),
)
})
}
func (s *Shell) settingsField(content layout.Widget) layout.Widget {
return s.settingsSurface(color.NRGBA{R: 225, G: 230, B: 238, A: 255}, unit.Dp(6), unit.Dp(10), content)
}
func (s *Shell) settingsActionBar(content layout.Widget) layout.Widget {
return s.settingsSurface(color.NRGBA{R: 215, G: 222, B: 232, A: 255}, unit.Dp(6), unit.Dp(8), content)
}
func (s *Shell) settingsFeedback(message string) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
if strings.TrimSpace(message) == "" {
return layout.Dimensions{}
}
return s.settingsSurface(color.NRGBA{R: 215, G: 222, B: 232, A: 255}, unit.Dp(6), unit.Dp(8), material.Caption(s.theme, message).Layout)(gtx)
}
}
func (s *Shell) settingsSurface(border color.NRGBA, radius, padding unit.Dp, content layout.Widget) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
return layout.Background{}.Layout(gtx,
func(gtx layout.Context) layout.Dimensions {
rect := image.Rectangle{Max: gtx.Constraints.Min}
if rect.Empty() {
return layout.Dimensions{Size: rect.Max}
}
paint.FillShape(gtx.Ops, border, clip.UniformRRect(rect, gtx.Dp(radius)).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(radius-unit.Dp(1))).Op(gtx.Ops))
}
return layout.Dimensions{Size: rect.Max}
},
func(gtx layout.Context) layout.Dimensions {
return layout.Inset{Top: padding, Right: padding, Bottom: padding, Left: padding}.Layout(gtx, content)
},
)
}
}
func (s *Shell) proxySettings(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.H6(s.theme, "代理").Layout),
layout.Rigid(material.Caption(s.theme, "仅保存无认证 scheme://host:port 端点;下拉框和实例表单显示完整地址。").Layout),
layout.Rigid(s.settingsField(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerSettings) })),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions { return s.proxyPickerField(gtx, proxyPickerSettings) }),
layout.Rigid(s.settingsField(s.formField("代理地址", "支持 http、https、socks4、socks5;不允许用户名、密码或路径", &s.proxyServer))),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(s.formField("代理地址", "支持 http、https、socks4、socks5;不允许用户名、密码或路径", &s.proxyServer)),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
layout.Rigid(s.settingsActionBar(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(material.Button(s.theme, &s.proxySave, "保存代理").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(s.proxyDeleteButton),
)
}),
})),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(material.Caption(s.theme, s.proxyFeedback).Layout),
layout.Rigid(s.settingsFeedback(s.proxyFeedback)),
)
}
@@ -1536,26 +1612,47 @@ func (s *Shell) proxyDeleteButton(gtx layout.Context) layout.Dimensions {
}
func (s *Shell) remoteDebugPortField(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Body2(s.theme, "远程调试起始端口").Layout),
layout.Rigid(material.Caption(s.theme, "留空时使用 9666;仅绑定 127.0.0.1,启动时顺序尝试下一个可用端口").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(material.Editor(s.theme, &s.remoteDebugPort, "例如:9666").Layout),
)
return s.settingsField(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.Body2(s.theme, "远程调试起始端口").Layout),
layout.Rigid(material.Caption(s.theme, "留空时使用 9666;仅绑定 127.0.0.1,启动时顺序尝试下一个可用端口").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
layout.Rigid(material.Editor(s.theme, &s.remoteDebugPort, "例如:9666").Layout),
)
})(gtx)
}
func (s *Shell) pathField(field PathField, label string, editor *widget.Editor, pick, search *widget.Clickable) layout.Widget {
return func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Inset{Right: unit.Dp(12)}.Layout(gtx, material.Body2(s.theme, label).Layout)
}),
layout.Flexed(1, material.Editor(s.theme, editor, "请输入或粘贴路径").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(material.Button(s.theme, pick, "选择").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(material.Button(s.theme, search, s.searchButtonLabel(field)).Layout),
)
return s.settingsField(func(gtx layout.Context) layout.Dimensions {
pathActions := func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Rigid(material.Button(s.theme, pick, "选择").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(6)}.Layout),
layout.Rigid(material.Button(s.theme, search, s.searchButtonLabel(field)).Layout),
)
}
children := []layout.FlexChild{
layout.Rigid(material.Body2(s.theme, label).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout),
}
if gtx.Constraints.Max.X <= gtx.Dp(640) {
children = append(children,
layout.Rigid(material.Editor(s.theme, editor, "请输入或粘贴路径").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(pathActions),
)
} else {
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Alignment: layout.Middle}.Layout(gtx,
layout.Flexed(1, material.Editor(s.theme, editor, "请输入或粘贴路径").Layout),
layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout),
layout.Rigid(pathActions),
)
}))
}
return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...)
})(gtx)
}
}