diff --git a/docs/06-tasks.md b/docs/06-tasks.md index 7f132ab..a74fc14 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -43,6 +43,7 @@ | T-305 | GUI 受管实例优雅停止与启停状态机 | T-304 | DONE | | T-306 | 精简代理下拉编辑与实例行安全操作布局 | T-304,T-305 | DONE | | T-307 | 受管浏览器意外退出监控与状态提示 | T-101,T-305,T-306 | DONE | +| T-308 | 设置页分区与控件边框层级 | T-306 | DONE | ## Backlog diff --git a/docs/current-state.md b/docs/current-state.md index 770aef4..06b5508 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -2,13 +2,14 @@ ## 快照 -- 日期:2026-07-25 -- 阶段:Phase 3 真实实例操作(T-301 至 T-307、T-201 至 T-208 已完成) +- 日期:2026-07-26 +- 阶段:Phase 3 真实实例操作(T-301 至 T-308、T-201 至 T-208 已完成) - 代码:已建立 Go module `chub`、`cmd/chub` 入口、logging 测试基座、浏览器 domain/application 合约、Chrome/Edge 参数/发现模块、启动 registry、Windows 身份/占用检查、loopback CDP 端口分配与端点校验、优雅关闭、Job Object、真实 Chrome/Edge smoke、JSON 配置存储、启动恢复、CLI JSON 合约、应用内事件总线、UI 状态测试和 Windows smoke 脚本,T-001 至 T-003、T-101 至 T-104、T-201 至 T-208、T-301 至 T-307 已完成 - UI:Gio 双页 Shell 使用左侧“实例/设置”导航;实例页以等宽“新建实例 / 刷新实例状态”命令区开始,窄内容区自动堆叠。列表固定显示实例名称、浏览器类型、用户数据目录、调试端口、状态和带边框的操作列;操作顺序固定为启动/停止(或重新检测)、编辑、删除,删除与相邻操作保持更大间距以降低误点。主操作在已退出时启动,在 Chub 托管运行或调试不可用时显示停止,在启动/停止中禁用,在外部关联、外部占用或未知占用时仅重新检测;优雅停止通过后台 adapter 验证 registry 的 PID/profile 身份并等待其释放,绝不按进程名关闭或接管外部 Chrome/Edge。刷新通过后台回调只检查已保存实例,以配置快照丢弃编辑或删除后的过期结果;它结合 Chub registry、指定 profile 的外部占用证据和 loopback CDP 端点更新状态,但不扫描、接管或关闭其他 Chrome/Edge。双击、Enter 或编辑图标打开实例编辑弹层,支持名称、浏览器类型、User Data Dir、启动 URL、完整地址代理选择和只读实际端口;保存保留未公开启动选项,Escape 对脏表单先请求确认,活跃/外部关联实例锁定身份约束字段。设置页使用完整代理地址选择器、地址输入及保存/删除操作;选择会回填地址,保存按稳定 `proxyId` 新增或更新,删除未引用代理前必须确认,仍被实例引用的代理会被拒绝。启动通过异步回调接到 Windows 浏览器启动器:未占用目录从已保存的起始端口(空值默认 9666)选择 loopback CDP 端口,并在启动时按已选代理 ID 解析最新的 `--proxy-server` 参数;同目录外部浏览器只有在 `DevToolsActivePort` 与 CDP 端点可验证时才显示“外部已关联”,且不会接管其生命周期。删除使用确认弹层,只删除 Chub 实例配置而不删除 User Data Dir。新建、编辑和删除实例会保存到本地配置;四个设置路径有独立异步可取消搜索,切换页面后输入和任务状态保留;新建实例表单使用 Label、Windows 原生目录选择器和 Chrome/Edge RadioButton;CLI `list/events` 已可用,`start/stop/restart` 等待 BrowserManager adapter - T-307:本应用会话启动的受管 Chrome/Edge 根进程由单实例后台 `Wait` 监控;意外退出按 ID、启动代次和 PID 验证后立即变为“已退出”,清空运行时 PID/端口,并以可关闭、可合并的提示告知用户。提示关闭后焦点回到启动操作;Chub 请求停止、外部实例、过期事件和应用关闭取消监控均不提示。 +- T-308:设置页以可滚动的外层工作区承载浏览器路径、默认目录、运行行为和代理分区;分区、字段、状态提示和保存操作区使用统一的浅色圆角描边。路径字段在紧凑内容区会将输入与“选择/搜索”操作纵向重排,切换页面后滚动、输入与异步搜索状态仍由持久 Gio widget 保留。 - 浏览器核心:设计参考来自 `D:\OPC\shop_helm\internal\platform\chrome`,尚未复制或接入本项目 -- blocker:无;T-307 已完成,下一项从 Backlog 或后续需求确定。 +- blocker:无;T-308 已完成,下一项从 Backlog 或后续需求确定。 ## 当前目录 diff --git a/docs/tasks/T-308.md b/docs/tasks/T-308.md new file mode 100644 index 0000000..88964b5 --- /dev/null +++ b/docs/tasks/T-308.md @@ -0,0 +1,35 @@ +--- +id: T-308 +title: 设置页分区与控件边框层级 +phase: 3 +deps: [T-306] +status: DONE +created: 2026-07-26 +owner: codex +--- + +## 需求与背景 + +设置页目前只有部分卡片和操作区具有边界,浏览器路径、目录、端口、代理和退出行为等组件在 Gio 实现中缺少清晰的视觉归属。需要在不改变现有保存、搜索、代理编辑或键盘交互的前提下,为设置模块整体及关键组件补充克制、一致的边框层级。 + +## 方案与边界 + +- 设置页采用三层层级:页面内容外层工作区、功能分区卡片、字段/状态/操作条。描边使用浅色 1dp、圆角与既有实例列表一致,不使用装饰性阴影。 +- 浏览器路径、默认目录、运行行为和代理分别具有可识别的分区;路径、端口和代理地址的标签、说明、输入与关联操作保持在同一个字段边界内。代理删除仍使用既有确认流程。 +- 底部“保存设置/取消”保持现有命令和顺序,仅以带上分隔线的操作区与表单内容分离。紧凑宽度下分区和字段按阅读顺序纵向排列,不裁剪按钮或文本。 +- 仅调整设置页的视觉容器和间距;不修改实例页、创建/编辑实例表单、配置格式、浏览器启动、代理校验及异步搜索逻辑。 +- 同步更新 `docs/ui/browser-manager.html`,作为设计交接基准;HTML 原型不代表 Gio 原生主题、DPI 或无障碍已完全验证。 + +## 验收要点 + +- 设置整体、浏览器路径、默认目录、运行行为、代理、路径字段、端口字段、状态提示和保存操作区均具有清晰且不过重的边框层级。 +- 既有四个路径的选择/搜索/取消搜索、远程调试起始端口、退出时关闭托管浏览器、代理选择/保存/删除确认、保存/取消设置行为保持不变。 +- 窄窗口按纵向重排,标签与帮助文本可读,按钮不被裁剪;Tab 顺序仍与视觉和阅读顺序一致。 +- HTML 原型脚本可解析;`gofmt`、`go test ./...`、`go vet ./...`、`go build -o build/chub.exe ./cmd/chub` 和 Windows smoke 通过。 + +## 执行记录 + +- 状态:DONE +- 变更:设置内容改为持久 `widget.List` 承载的可滚动工作区;Gio 使用外层工作区、路径/目录/运行/代理分区卡片、字段容器、状态提示与操作条的浅色圆角 1dp 边框。路径字段在窄内容区会将编辑框与“选择/搜索”操作纵向重排,既有路径搜索、代理编辑和保存命令不变。HTML 原型同步了相同的工作区、卡片、字段、状态和操作区边框层级。 +- 验证:`gofmt -w internal/ui/shell.go`、`go test ./internal/ui`、HTML 内嵌脚本语法检查、`scripts/smoke-windows.ps1`(含 `go test ./...`、`go vet ./...`、Windows 构建和 CLI JSON contract)均通过。 +- 阻塞:无。 diff --git a/docs/ui/browser-manager.html b/docs/ui/browser-manager.html index b694661..e2bb400 100644 --- a/docs/ui/browser-manager.html +++ b/docs/ui/browser-manager.html @@ -174,16 +174,22 @@ .toast { position: fixed; right: 22px; bottom: 22px; z-index: 30; max-width: min(420px, calc(100vw - 44px)); padding: 12px 15px; border: 1px solid var(--line); border-left: 4px solid var(--accent); border-radius: 10px; background: var(--surface); box-shadow: var(--shadow); color: var(--text); opacity: 0; transform: translateY(8px); pointer-events: none; transition: opacity .16s ease, transform .16s ease; } .toast.show { opacity: 1; transform: translateY(0); } .page-hidden { display: none !important; } - .settings-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, .8fr); gap: 18px; } - .settings-card { background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius-card); padding: 22px; display: grid; gap: 20px; } + .settings-workspace { padding: 16px; border: 1px solid var(--line); border-radius: calc(var(--radius-card) + 2px); background: var(--surface-2); } + .settings-grid { display: grid; grid-template-columns: minmax(0, 1.2fr) minmax(260px, .8fr); gap: 16px; } + .settings-card { background: var(--surface); border: 1px solid var(--line-strong); border-radius: var(--radius-card); padding: 20px; display: grid; gap: 16px; } .settings-card h2 { margin: 0; font-size: 18px; letter-spacing: -.02em; } - .settings-card > p { margin: -12px 0 0; color: var(--muted); line-height: 1.5; } + .settings-card > p { margin: -10px 0 0; color: var(--muted); line-height: 1.5; } + .settings-card .form-section, .settings-card .setting-list { padding: 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); } + .settings-card .form-section { gap: 12px; } + .settings-card .field { padding: 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); } + .settings-card .field:focus-within { border-color: var(--accent); box-shadow: var(--focus); } + .settings-card .check-line { padding: 12px; border: 1px solid var(--line); border-radius: 9px; background: var(--surface); } .path-field { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; gap: 8px; } .radio-row { display: flex; flex-wrap: wrap; gap: 16px; align-items: center; min-height: 38px; } .radio-row label { font-weight: 500; display: inline-flex; gap: 7px; align-items: center; } - .settings-status { min-height: 42px; padding: 11px 12px; border-radius: 9px; background: var(--success-soft); color: var(--success); font-size: 12px; line-height: 1.45; } + .settings-status { min-height: 42px; padding: 11px 12px; border: 1px solid color-mix(in srgb, var(--success) 35%, var(--line)); border-radius: 9px; background: var(--success-soft); color: var(--success); font-size: 12px; line-height: 1.45; } .settings-status.warning { background: var(--warning-soft); color: var(--warning); } - .settings-actions { display: flex; justify-content: flex-end; gap: 8px; border-top: 1px solid var(--line); padding-top: 18px; } + .settings-actions { display: flex; justify-content: flex-end; gap: 8px; border: 1px solid var(--line); border-radius: 10px; background: var(--surface-2); padding: 12px; } .setting-list { display: grid; gap: 12px; } .setting-list .detail-line { padding-bottom: 12px; border-bottom: 1px solid var(--line); } .setting-list .detail-line:last-child { border-bottom: 0; padding-bottom: 0; } @@ -253,7 +259,7 @@
-
+

浏览器与目录

这些设置用于新建实例的默认值;已有实例的配置不会被静默修改。

浏览器路径

@@ -268,7 +274,7 @@
-
+
diff --git a/internal/ui/shell.go b/internal/ui/shell.go index 8ea1f66..9310045 100644 --- a/internal/ui/shell.go +++ b/internal/ui/shell.go @@ -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) } }