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:
+106
-54
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user