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 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-23 21:40:18 +08:00
co-authored by Claude Opus 4.8
parent 03d3f83f1d
commit 58fd92aa80
2 changed files with 115 additions and 54 deletions
+106 -54
View File
@@ -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)