From 58fd92aa80856175681f3b9f5cd36faed1d88756 Mon Sep 17 00:00:00 2001 From: ila Date: Thu, 23 Jul 2026 21:40:18 +0800 Subject: [PATCH] feat(v2): T-308 group monitor/settings into white cards for visual separation Adds a card() surface (white rounded panel, 1px #C8D4E3 stroke, 8px radius) matching the light-Windows spec, and groups the monitor (video / status+ controls / gallery) and settings (camera / params / deps) into cards so the gray-blue page shows through the gutters. Components were previously flat on one background with no separation. Cross-compiles for Windows. Co-Authored-By: Claude Opus 4.8 --- progress.md | 9 +++ v2/internal/ui/window.go | 160 ++++++++++++++++++++++++++------------- 2 files changed, 115 insertions(+), 54 deletions(-) diff --git a/progress.md b/progress.md index 6d5b1ef..7f852f9 100644 --- a/progress.md +++ b/progress.md @@ -622,3 +622,12 @@ - 阻塞:`cmd/silver-pose` 全量构建需 Windows+CGo(onnxruntime)+Gio;窗口目视/表单交互需 Windows。 - 决策:账号密码按用户要求进设置(内网演示),但仅存未跟踪 config.local.json、UI 掩码、不入日志/版本/事件;source.id 用固定/原值避免把内网 IP 写进 JSONL。设置改动"下次开始监控生效"经控制器重载 config 实现(对齐 V1 草稿→运行快照语义)。 - 下一步:用户 Windows `go run ./cmd/silver-pose` 目视——80% 窗口、2×3 画廊、Esc 关图、按钮灰显、设置页可编辑并保存后重启监控生效。 + +## 【2026-07-23】T-308 收尾:监控/设置组件加白色卡片边框 + +- 状态:DOING(卡片布局代码完成 + 交叉编译通过;观感需 Windows 目视) +- 变更:新增 `card()`(白圆角面板 `#FFFFFF` + `#C8D4E3` 1px 细边 + 8px 圆角,`op.Record` 量出内容再画底/描边),对齐 UI 规范"浅灰蓝底+白卡片"。监控页分三卡:视频卡、状态控制卡(运行/检测状态+开始停止+事件)、画廊卡(标题+2×3 缩略图);设置页分三卡:摄像头连接、常用参数、运行依赖。新增 `verticalRows` 给卡内行加统一间距。此前组件平铺在同一底色上、无分隔,偏离了规范;本次补齐。 +- 验证:`gofmt` 干净;`GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -mod=vendor ./internal/ui/... ./cmd/ui-spike/...` 退出码 0(含 `card()` 的 `op.Record`/`clip.UniformRRect.Push` 等 API)。 +- 阻塞:卡片间距/描边深浅、缩略图是否再加细边等观感需 Windows 目视微调。 +- 决策:按功能分组成少量卡(视频/状态控制/画廊、摄像头/参数/依赖),不逐行套框以免过碎;卡间留 8–12px 浅灰蓝缝。 +- 下一步:用户 Windows 目视卡片分隔观感;据反馈微调后 T-308 置 DONE。 diff --git a/v2/internal/ui/window.go b/v2/internal/ui/window.go index 10f1ca7..a3ac1b9 100644 --- a/v2/internal/ui/window.go +++ b/v2/internal/ui/window.go @@ -474,10 +474,12 @@ func (window *Window) layoutMonitor(gtx layout.Context, model ViewModel, hasFram return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, layout.Flexed(7, func(gtx layout.Context) layout.Dimensions { return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - if !hasFrame { - return layout.Center.Layout(gtx, material.Body1(window.theme, model.ConnectionText).Layout) - } - return widget.Image{Src: window.imageOp, Fit: widget.Contain, Position: layout.Center}.Layout(gtx) + return card(gtx, func(gtx layout.Context) layout.Dimensions { + if !hasFrame { + return layout.Center.Layout(gtx, material.Body1(window.theme, model.ConnectionText).Layout) + } + return widget.Image{Src: window.imageOp, Fit: widget.Contain, Position: layout.Center}.Layout(gtx) + }) }) }), layout.Flexed(3, func(gtx layout.Context) layout.Dimensions { @@ -493,57 +495,86 @@ func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel, runn if running && window.stopButton.Clicked(gtx) { window.stop() } - return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { return layout.Flex{Axis: layout.Vertical}.Layout(gtx, - // Row 1: connection status and detection status on one line. layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(material.Body1(window.theme, model.ConnectionText).Layout), - layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - label := material.Body1(window.theme, model.StateText) - if model.Critical { - label.Color = colorAlert - } - return label.Layout(gtx) - }), - ) + return card(gtx, func(gtx layout.Context) layout.Dimensions { + return window.statusControlsGroup(gtx, model, running) + }) }), layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - // Row 2: compact start/stop buttons on one line. - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return stateButton(gtx, window.theme, &window.startButton, "开始监控", !running) - }), - layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return stateButton(gtx, window.theme, &window.stopButton, "停止监控", running) - }), - ) + layout.Flexed(1, func(gtx layout.Context) layout.Dimensions { + return card(gtx, window.galleryGroup) }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - // Row 3: event text and the red-note on one line. - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, - layout.Rigid(material.Body1(window.theme, model.EventText).Layout), - layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout), - layout.Rigid(func(gtx layout.Context) layout.Dimensions { - note := material.Body2(window.theme, "红色=确认摔倒") - note.Color = colorSubtle - return note.Layout(gtx) - }), - ) - }), - layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), - layout.Rigid(material.Body2(window.theme, "最近确认截图(双击看原图)").Layout), - layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), - // Remaining area: 3x3 reverse-chronological screenshot gallery. - layout.Flexed(1, window.layoutGallery), ) }) } +func (window *Window) statusControlsGroup(gtx layout.Context, model ViewModel, running bool) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(material.Body1(window.theme, model.ConnectionText).Layout), + layout.Rigid(layout.Spacer{Width: unit.Dp(16)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + label := material.Body1(window.theme, model.StateText) + if model.Critical { + label.Color = colorAlert + } + return label.Layout(gtx) + }), + ) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal}.Layout(gtx, + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return stateButton(gtx, window.theme, &window.startButton, "开始监控", !running) + }), + layout.Rigid(layout.Spacer{Width: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return stateButton(gtx, window.theme, &window.stopButton, "停止监控", running) + }), + ) + }), + layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Horizontal, Alignment: layout.Middle}.Layout(gtx, + layout.Rigid(material.Body1(window.theme, model.EventText).Layout), + layout.Rigid(layout.Spacer{Width: unit.Dp(12)}.Layout), + layout.Rigid(func(gtx layout.Context) layout.Dimensions { + note := material.Body2(window.theme, "红色=确认摔倒") + note.Color = colorSubtle + return note.Layout(gtx) + }), + ) + }), + ) +} + +func (window *Window) galleryGroup(gtx layout.Context) layout.Dimensions { + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, + layout.Rigid(material.Body2(window.theme, "最近确认截图(双击看原图)").Layout), + layout.Rigid(layout.Spacer{Height: unit.Dp(4)}.Layout), + layout.Flexed(1, window.layoutGallery), + ) +} + +// card wraps content in a white rounded surface with a subtle stroke, so the +// gray-blue page shows through the gutters between components (per the UI spec). +func card(gtx layout.Context, inner layout.Widget) layout.Dimensions { + border := widget.Border{Color: colorStroke, Width: unit.Dp(1), CornerRadius: unit.Dp(8)} + return border.Layout(gtx, func(gtx layout.Context) layout.Dimensions { + macro := op.Record(gtx.Ops) + dims := layout.UniformInset(unit.Dp(10)).Layout(gtx, inner) + content := macro.Stop() + defer clip.UniformRRect(image.Rectangle{Max: dims.Size}, gtx.Dp(8)).Push(gtx.Ops).Pop() + paint.Fill(gtx.Ops, colorSurface) + content.Add(gtx.Ops) + return dims + }) +} + func smallButton(gtx layout.Context, theme *material.Theme, click *widget.Clickable, label string) layout.Dimensions { return stateButton(gtx, theme, click, label, true) } @@ -660,7 +691,7 @@ func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout if window.saveParams.Clicked(gtx) { window.onSaveParams() } - rows := []layout.Widget{ + cameraCard := verticalRows( window.heading("摄像头连接(账号密码仅存本地配置,下次启动生效)"), window.editorRow("IP / 主机", &window.editHost), window.editorRow("端口", &window.editPort), @@ -671,7 +702,8 @@ func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout window.editorRow("连接超时 (秒)", &window.editTimeout), window.checkRow("低延迟", &window.lowLatency), window.buttonRow(&window.saveCamera, "保存摄像头"), - window.spacerRow(), + ) + paramsCard := verticalRows( window.heading("常用检测参数(下次启动生效)"), window.editorRow("关键点置信度", &window.editKeypoint), window.editorRow("确认窗口 (秒, 1-3)", &window.editConfirm), @@ -681,18 +713,38 @@ func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout window.checkRow("要求膝踝清晰", &window.requireLower), window.buttonRow(&window.saveParams, "保存参数"), window.labelRow(window.settingsStatus), - window.spacerRow(), + ) + depsCard := verticalRows( window.heading("运行依赖(只读)"), - window.labelRow("模型 SHA-256:" + shortHash(window.settings.ModelSHA256)), - window.labelRow("运行依赖:" + window.settings.RuntimeSummary), + window.labelRow("模型 SHA-256:"+shortHash(window.settings.ModelSHA256)), + window.labelRow("运行依赖:"+window.settings.RuntimeSummary), + ) + sections := []layout.Widget{ + func(gtx layout.Context) layout.Dimensions { return card(gtx, cameraCard) }, + func(gtx layout.Context) layout.Dimensions { return card(gtx, paramsCard) }, + func(gtx layout.Context) layout.Dimensions { return card(gtx, depsCard) }, } - return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { - return material.List(window.theme, &window.settingsList).Layout(gtx, len(rows), func(gtx layout.Context, index int) layout.Dimensions { - return layout.UniformInset(unit.Dp(4)).Layout(gtx, rows[index]) + return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions { + return material.List(window.theme, &window.settingsList).Layout(gtx, len(sections), func(gtx layout.Context, index int) layout.Dimensions { + return layout.UniformInset(unit.Dp(6)).Layout(gtx, sections[index]) }) }) } +// verticalRows stacks row widgets with a small gap between them. +func verticalRows(rows ...layout.Widget) layout.Widget { + return func(gtx layout.Context) layout.Dimensions { + children := make([]layout.FlexChild, 0, len(rows)*2) + for i := range rows { + if i > 0 { + children = append(children, layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout)) + } + children = append(children, layout.Rigid(rows[i])) + } + return layout.Flex{Axis: layout.Vertical}.Layout(gtx, children...) + } +} + func (window *Window) heading(text string) layout.Widget { return func(gtx layout.Context) layout.Dimensions { return material.Body1(window.theme, text).Layout(gtx)