feat(v2): migrate UI from Walk to Gio

Rewrites internal/ui/window.go in Gio, keeping the Window interface
(NewWindow/Run/Present/PresentAlert/ReportIssue) and the pure-Go ViewModel.
Background updates mutate shared state under a lock and Invalidate; the Gio
loop renders the latest frame. Dual tabs, widget.Image for render's RGBA, a
self-drawn confirm dialog and a MessageBeep sound. Drops lxn/walk (and the
TTM_ADDTOOL/comctl class of problems). Verified via GOOS=windows CGO_ENABLED=0
build of the UI packages; full app build needs Windows+CGo for onnxruntime.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-23 14:26:19 +08:00
co-authored by Claude Opus 4.8
parent 18e1331f8f
commit 4188a2b06c
6 changed files with 340 additions and 147 deletions
+9
View File
@@ -559,3 +559,12 @@
- 阻塞:真实窗口/声音/弹窗目视需 Windows。 - 阻塞:真实窗口/声音/弹窗目视需 Windows。
- 决策:后台 `Present/PresentAlert/ReportIssue` 只在带锁共享区更新最新帧/状态/待弹窗并 `Invalidate()`,Gio 事件循环在独立 goroutine 只渲染最新帧(取代 Walk 的 `main.Synchronize`);声音用 Windows `MessageBeep` syscall(`sound_windows.go` + 非 Windows 空实现),弹窗自绘遮罩。`ViewModel` 逻辑复用、`spec_test.go` 不受影响。 - 决策:后台 `Present/PresentAlert/ReportIssue` 只在带锁共享区更新最新帧/状态/待弹窗并 `Invalidate()`,Gio 事件循环在独立 goroutine 只渲染最新帧(取代 Walk 的 `main.Synchronize`);声音用 Windows `MessageBeep` syscall(`sound_windows.go` + 非 Windows 空实现),弹窗自绘遮罩。`ViewModel` 逻辑复用、`spec_test.go` 不受影响。
- 下一步:写 Gio `window.go` + sound 文件,交叉编译通过后接 `cmd/silver-pose` 验证。 - 下一步:写 Gio `window.go` + sound 文件,交叉编译通过后接 `cmd/silver-pose` 验证。
## 【2026-07-23】T-306 V2 UI 从 Walk 迁移到 Gio(代码完成 + 交叉编译验证)
- 状态:DOING(迁移代码完成 + Windows 交叉编译通过;窗口/声音/弹窗目视需 Windows)
- 变更:用 Gio 重写 `v2/internal/ui/window.go`,保持 `Window` 公开接口(`NewWindow/Run/Present/PresentAlert/ReportIssue`)与 `spec.go` 的 `ViewModel`/`MonitorWindowSpec` 不变。后台 `Present/PresentAlert/ReportIssue` 只在带锁共享区更新最新帧/状态/待弹窗并 `Invalidate()`;Gio 事件循环在独立 goroutine 渲染最新帧(取代 Walk 的 `main.Synchronize`)。双 Tab(监控/设置)、`widget.Image` 贴 render 的 `*image.RGBA`、确认弹窗自绘遮罩+“我已知晓”、声音用 `MessageBeep`(`sound_windows.go` syscall + `sound_other.go` 空实现)。`cmd/silver-pose`/`monitor`/`alert` 未改。`go mod tidy` 移除 `lxn/walk`、`lxn/win`、`govaluate.v3`,新增 `golang.org/x/sys`(声音)。
- 验证:`GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build ./internal/ui/... ./cmd/ui-spike/... ./cmd/gio-spike/...` → 全部退出码 0(Gio Windows 版无 CGo,可对真实 Windows API 型检)。`grep lxn/walk` 全仓库无剩余引用。`cmd/silver-pose` 全量构建需 Windows + `CGO_ENABLED=1`(onnxruntime_go 依赖 CGo,迁移前即如此),本环境无 mingw 故不构建。
- 阻塞:真实窗口显示、声音、弹窗目视需 Windows。`go run ./cmd/ui-spike` 为 CGo-free,可单独目视迁移后的 Gio 窗口外壳;`go run ./cmd/silver-pose` 需 `CGO_ENABLED=1` 起全量管线。
- 决策:`internal/ui` 正式由 Walk 迁至 Gio,彻底消除 comctl/manifest(`TTM_ADDTOOL`)问题;UI 只渲染已判定的最新帧,事件计算仍在后台。
- 下一步:用户 Windows 目视 `go run ./cmd/ui-spike`(外壳)与 `go run ./cmd/silver-pose`(全量,`CGO_ENABLED=1`)确认起窗、贴视频、双 Tab、确认弹窗与声音;通过则 T-306 与 T-304 一并推进 DONE。
+1 -4
View File
@@ -4,10 +4,9 @@ go 1.24.0
require ( require (
gioui.org v0.10.1 gioui.org v0.10.1
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794
github.com/lxn/win v0.0.0-20210218163916-a377121e959e
github.com/yalue/onnxruntime_go v1.31.0 github.com/yalue/onnxruntime_go v1.31.0
golang.org/x/image v0.26.0 golang.org/x/image v0.26.0
golang.org/x/sys v0.39.0
) )
require ( require (
@@ -15,7 +14,5 @@ require (
github.com/go-text/typesetting v0.3.4 // indirect github.com/go-text/typesetting v0.3.4 // indirect
golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/exp/shiny v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
golang.org/x/net v0.48.0 // indirect golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect golang.org/x/text v0.32.0 // indirect
gopkg.in/Knetic/govaluate.v3 v3.0.0 // indirect
) )
-7
View File
@@ -9,10 +9,6 @@ github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaF
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY= github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos= github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o= github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794 h1:NVRJ0Uy0SOFcXSKLsS65OmI1sgCCfiDUPj+cwnH7GZw=
github.com/lxn/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
github.com/yalue/onnxruntime_go v1.31.0 h1:1ln4YW1SFOFfGJZXe3jNOb2JUSt+l2pEneZfV8HdtFA= github.com/yalue/onnxruntime_go v1.31.0 h1:1ln4YW1SFOFfGJZXe3jNOb2JUSt+l2pEneZfV8HdtFA=
github.com/yalue/onnxruntime_go v1.31.0/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4= github.com/yalue/onnxruntime_go v1.31.0/go.mod h1:b4X26A8pekNb1ACJ58wAXgNKeUCGEAQ9dmACut9Sm/4=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
@@ -23,10 +19,7 @@ golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c= golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/Knetic/govaluate.v3 v3.0.0 h1:18mUyIt4ZlRlFZAAfVetz4/rzlJs9yhN+U02F4u1AOc=
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
+7
View File
@@ -0,0 +1,7 @@
//go:build !windows
package ui
// playAlertSound is a no-op off Windows; the V2 UI targets Windows and uses the
// system beep there. This stub keeps the package building for cross-checks.
func playAlertSound() {}
+17
View File
@@ -0,0 +1,17 @@
//go:build windows
package ui
import "golang.org/x/sys/windows"
// MB_ICONHAND selects the system critical-stop sound for a confirmed fall.
const mbIconHand = 0x00000010
var (
user32 = windows.NewLazySystemDLL("user32.dll")
procMessageBeep = user32.NewProc("MessageBeep")
)
func playAlertSound() {
_, _, _ = procMessageBeep.Call(uintptr(mbIconHand))
}
+306 -136
View File
@@ -2,17 +2,40 @@ package ui
import ( import (
"fmt" "fmt"
"image"
"image/color"
"log"
"os"
"strings" "strings"
"sync"
"github.com/lxn/walk" "gioui.org/app"
d "github.com/lxn/walk/declarative" "gioui.org/font/gofont"
"github.com/lxn/win" "gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
"gioui.org/op/paint"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
"gioui.org/widget/material"
"silverpose/v2/internal/alert" "silverpose/v2/internal/alert"
"silverpose/v2/internal/fall" "silverpose/v2/internal/fall"
"silverpose/v2/internal/monitor" "silverpose/v2/internal/monitor"
) )
// Light Windows theme tokens (see docs/ui/silver-pose-ui-ux-spec.md).
var (
colorBg = color.NRGBA{R: 0xEA, G: 0xF1, B: 0xF8, A: 0xFF}
colorSurface = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}
colorText = color.NRGBA{R: 0x1E, G: 0x29, B: 0x3B, A: 0xFF}
colorSubtle = color.NRGBA{R: 0x52, G: 0x65, B: 0x7C, A: 0xFF}
colorAccent = color.NRGBA{R: 0x25, G: 0x63, B: 0xEB, A: 0xFF}
colorAlert = color.NRGBA{R: 0xC6, G: 0x28, B: 0x28, A: 0xFF}
colorScrim = color.NRGBA{A: 0x8C}
)
type Settings struct { type Settings struct {
SourceEnvironment string SourceEnvironment string
ModelSHA256 string ModelSHA256 string
@@ -25,175 +48,322 @@ type Commands struct {
Stop func() Stop func()
} }
// Window is an observer of monitor updates. It does not own a stream, runtime, // Window is an observer of monitor updates. It owns no stream, runtime, fall
// fall engine, or event artifact writer. // engine or artifact writer. Background goroutines call Present/PresentAlert/
// ReportIssue, which only mutate shared state under a lock and Invalidate; the
// Gio event loop (its own goroutine) reads that state and renders the newest
// frame. This replaces Walk's main.Synchronize and its comctl/manifest coupling.
type Window struct { type Window struct {
main *walk.MainWindow appWindow *app.Window
imageView *walk.ImageView theme *material.Theme
connectionLabel *walk.TextLabel settings Settings
stateLabel *walk.TextLabel commands Commands
eventLabel *walk.TextLabel spec WindowSpec
startButton *walk.PushButton
stopButton *walk.PushButton mu sync.Mutex
model ViewModel model ViewModel
bitmap *walk.Bitmap latestImage *image.RGBA
seenAlerts map[string]struct{} imageGen uint64
commands Commands pending *alert.Record
seenAlerts map[string]struct{}
// Touched only by the event-loop goroutine.
imageOp paint.ImageOp
imageShownGen uint64
tabs []string
tabButton []widget.Clickable
active int
startButton widget.Clickable
stopButton widget.Clickable
ackButton widget.Clickable
running bool
} }
func NewWindow(settings Settings, commands Commands) (*Window, error) { func NewWindow(settings Settings, commands Commands) (*Window, error) {
spec := MonitorWindowSpec() spec := MonitorWindowSpec()
window := &Window{model: NewViewModel(), seenAlerts: make(map[string]struct{}), commands: commands} theme := material.NewTheme()
window.model.SetSourceReady(settings.SourceEnvironment, settings.SourceEnvironment != "") theme.Shaper = text.NewShaper(text.WithCollection(gofont.Collection()))
background := d.SolidColorBrush{Color: walk.RGB(234, 241, 248)} theme.Palette.Bg = colorBg
surface := d.SolidColorBrush{Color: walk.RGB(255, 255, 255)} theme.Palette.Fg = colorText
if err := (d.MainWindow{ theme.Palette.ContrastBg = colorAccent
AssignTo: &window.main, theme.Palette.ContrastFg = color.NRGBA{R: 0xFF, G: 0xFF, B: 0xFF, A: 0xFF}
Title: spec.Title,
Size: d.Size{Width: spec.Width, Height: spec.Height}, model := NewViewModel()
MinSize: d.Size{Width: 960, Height: 640}, model.SetSourceReady(settings.SourceEnvironment, settings.SourceEnvironment != "")
Background: background,
Layout: d.VBox{}, appWindow := new(app.Window)
Children: []d.Widget{ appWindow.Option(
d.TabWidget{ app.Title(spec.Title),
Pages: []d.TabPage{ app.Size(unit.Dp(spec.Width), unit.Dp(spec.Height)),
{ app.MinSize(unit.Dp(960), unit.Dp(640)),
Title: "监控", )
Layout: d.HBox{}, window := &Window{
Children: []d.Widget{ appWindow: appWindow,
d.Composite{ theme: theme,
Background: surface, settings: settings,
Layout: d.VBox{}, commands: commands,
StretchFactor: 3, spec: spec,
Children: []d.Widget{ model: model,
d.TextLabel{Text: "实时画面", ToolTipText: "显示完成推理与姿态叠加后的最新画面"}, seenAlerts: make(map[string]struct{}),
d.ImageView{AssignTo: &window.imageView, Mode: d.ImageViewModeZoom, StretchFactor: 8, ToolTipText: "实时姿态画面"}, tabs: []string{"监控", "设置"},
},
},
d.Composite{
Background: surface,
Layout: d.VBox{},
StretchFactor: 2,
Children: []d.Widget{
d.TextLabel{Text: "运行状态"},
d.TextLabel{AssignTo: &window.connectionLabel, Text: window.model.ConnectionText},
d.TextLabel{AssignTo: &window.stateLabel, Text: window.model.StateText},
d.TextLabel{AssignTo: &window.eventLabel, Text: window.model.EventText},
d.PushButton{AssignTo: &window.startButton, Text: "开始监控", ToolTipText: "使用当前配置启动单路监控", OnClicked: window.start},
d.PushButton{AssignTo: &window.stopButton, Text: "停止监控", Enabled: false, ToolTipText: "停止监控并释放本地运行资源", OnClicked: window.stop},
d.TextLabel{Text: "红色仅表示已确认摔倒。连接异常不会触发报警。"},
},
},
},
},
{
Title: "设置",
Layout: d.VBox{},
Children: []d.Widget{
d.TextLabel{Text: "来源与运行依赖(只读,不显示 RTSP 地址或密码)"},
d.TextLabel{Text: window.model.SettingsSummary},
d.TextLabel{Text: "模型 SHA-256:" + shortHash(settings.ModelSHA256)},
d.TextLabel{Text: "运行依赖:" + settings.RuntimeSummary},
d.TextLabel{Text: "事件参数:" + settings.EventSummary},
d.TextLabel{Text: "参数在配置文件中维护;修改后请停止并重新开始监控。"},
},
},
},
},
},
}).Create(); err != nil {
return nil, err
} }
window.main.Closing().Attach(func(canceled *bool, reason walk.CloseReason) { window.tabButton = make([]widget.Clickable, len(window.tabs))
window.stop()
if window.bitmap != nil {
window.bitmap.Dispose()
window.bitmap = nil
}
})
return window, nil return window, nil
} }
func (window *Window) Run() int { return window.main.Run() } // Run drives the platform event loop on the calling (main) goroutine and the
// window loop on a separate goroutine. It does not return; the process exits
// after the window is closed and Stop cleanup has run.
func (window *Window) Run() int {
go func() {
err := window.loop()
window.stop()
if err != nil {
log.Print(err)
}
os.Exit(0)
}()
app.Main()
return 0
}
func (window *Window) loop() error {
var ops op.Ops
for {
switch event := window.appWindow.Event().(type) {
case app.DestroyEvent:
return event.Err
case app.FrameEvent:
gtx := app.NewContext(&ops, event)
window.layout(gtx)
event.Frame(gtx.Ops)
}
}
}
func (window *Window) start() { func (window *Window) start() {
if window.running {
return
}
window.running = true
if window.commands.Start != nil { if window.commands.Start != nil {
window.commands.Start() window.commands.Start()
} }
window.startButton.SetEnabled(false)
window.stopButton.SetEnabled(true)
} }
func (window *Window) stop() { func (window *Window) stop() {
if window.commands.Stop != nil { if window.commands.Stop != nil {
window.commands.Stop() window.commands.Stop()
} }
if window.startButton != nil { window.running = false
window.startButton.SetEnabled(true)
}
if window.stopButton != nil {
window.stopButton.SetEnabled(false)
}
} }
// Present runs only the final state assignment on the UI thread. // Present stores the newest analysed frame and state, then requests a redraw.
func (window *Window) Present(update monitor.Update) { func (window *Window) Present(update monitor.Update) {
window.main.Synchronize(func() { window.mu.Lock()
state := highestState(update.Result) state := highestState(update.Result)
window.model.Apply(state, update.SourceStatus) window.model.Apply(state, update.SourceStatus)
if update.SourceMessage != "" { if update.SourceMessage != "" {
window.model.ConnectionText = "视频流:" + update.SourceMessage window.model.ConnectionText = "视频流:" + update.SourceMessage
} }
_ = window.connectionLabel.SetText(window.model.ConnectionText) if update.Image != nil {
_ = window.stateLabel.SetText(window.model.StateText) window.latestImage = update.Image
_ = window.eventLabel.SetText(window.model.EventText) window.imageGen++
if update.Image == nil { }
return window.mu.Unlock()
} window.appWindow.Invalidate()
bitmap, err := walk.NewBitmapFromImage(update.Image)
if err != nil {
return
}
if err := window.imageView.SetImage(bitmap); err != nil {
bitmap.Dispose()
return
}
previous := window.bitmap
window.bitmap = bitmap
if previous != nil {
previous.Dispose()
}
})
} }
// PresentAlert is called by a consumer of Monitor.Alerts. Event IDs are checked // PresentAlert queues one confirm popup per event ID and plays the alert sound.
// again at the UI boundary so a reconnect/repaint cannot replay sound or popup.
func (window *Window) PresentAlert(record alert.Record) { func (window *Window) PresentAlert(record alert.Record) {
if !record.Written { if !record.Written {
return return
} }
window.main.Synchronize(func() { window.mu.Lock()
if _, seen := window.seenAlerts[record.Event.EventID]; seen { if _, seen := window.seenAlerts[record.Event.EventID]; seen {
return window.mu.Unlock()
return
}
window.seenAlerts[record.Event.EventID] = struct{}{}
queued := record
window.pending = &queued
window.mu.Unlock()
playAlertSound()
window.appWindow.Invalidate()
}
// ReportIssue surfaces a recoverable startup/worker issue without red fall
// language or exposing configuration values.
func (window *Window) ReportIssue(message string) {
window.mu.Lock()
window.model.ConnectionText = "视频流:" + message
window.model.Critical = false
window.model.StateText = "检测状态:正常"
window.mu.Unlock()
window.appWindow.Invalidate()
}
func (window *Window) layout(gtx layout.Context) layout.Dimensions {
paint.Fill(gtx.Ops, colorBg)
window.mu.Lock()
model := window.model
image := window.latestImage
gen := window.imageGen
pending := window.pending
window.mu.Unlock()
hasImage := image != nil
if hasImage && gen != window.imageShownGen {
window.imageOp = paint.NewImageOp(image)
window.imageShownGen = gen
}
content := func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(window.layoutTabs),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if window.active == 0 {
return window.layoutMonitor(gtx, model, hasImage)
}
return window.layoutSettings(gtx, model)
}),
)
}
if pending == nil {
return content(gtx)
}
return layout.Stack{}.Layout(gtx,
layout.Expanded(content),
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
paint.FillShape(gtx.Ops, colorScrim, clip.Rect{Max: gtx.Constraints.Min}.Op())
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
return layout.Center.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return window.layoutDialog(gtx, *pending)
})
}),
)
}
func (window *Window) layoutTabs(gtx layout.Context) layout.Dimensions {
for i := range window.tabButton {
if window.tabButton[i].Clicked(gtx) {
window.active = i
} }
window.seenAlerts[record.Event.EventID] = struct{}{} }
win.MessageBeep(win.MB_ICONHAND) children := make([]layout.FlexChild, 0, len(window.tabs))
message := fmt.Sprintf("确认摔倒\n人员:%s\n延迟:%.2f 秒\n截图:%s", record.Event.TrackID, record.Event.LatencySeconds, record.ScreenshotPath) for i := range window.tabs {
walk.MsgBox(window.main, "Silver Pose V2 · 确认摔倒", message, walk.MsgBoxOK|walk.MsgBoxIconError) i := i
children = append(children, layout.Rigid(func(gtx layout.Context) layout.Dimensions {
button := material.Button(window.theme, &window.tabButton[i], window.tabs[i])
if i != window.active {
button.Background = colorSurface
button.Color = colorSubtle
}
return layout.UniformInset(unit.Dp(6)).Layout(gtx, button.Layout)
}))
}
return layout.Flex{}.Layout(gtx, children...)
}
func (window *Window) layoutMonitor(gtx layout.Context, model ViewModel, hasImage bool) layout.Dimensions {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Flexed(3, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
if !hasImage {
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(2, func(gtx layout.Context) layout.Dimensions {
return window.layoutStatus(gtx, model)
}),
)
}
func (window *Window) layoutStatus(gtx layout.Context, model ViewModel) layout.Dimensions {
if window.startButton.Clicked(gtx) {
window.start()
}
if window.stopButton.Clicked(gtx) {
window.stop()
}
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(material.H6(window.theme, "运行状态").Layout),
layout.Rigid(material.Body1(window.theme, model.ConnectionText).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(material.Body1(window.theme, model.EventText).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(material.Button(window.theme, &window.startButton, "开始监控").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(6)}.Layout),
layout.Rigid(material.Button(window.theme, &window.stopButton, "停止监控").Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(material.Body2(window.theme, "红色仅表示已确认摔倒。连接异常不会触发报警。").Layout),
)
}) })
} }
// ReportIssue communicates a recoverable startup/worker issue without using func (window *Window) layoutSettings(gtx layout.Context, model ViewModel) layout.Dimensions {
// critical-red fall language or exposing configuration values. return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
func (window *Window) ReportIssue(message string) { return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
window.main.Synchronize(func() { layout.Rigid(material.H6(window.theme, "来源与运行依赖(只读,不显示 RTSP 地址或密码)").Layout),
window.model.ConnectionText = "视频流:" + message layout.Rigid(material.Body1(window.theme, model.SettingsSummary).Layout),
window.model.Critical = false layout.Rigid(material.Body1(window.theme, "模型 SHA-256:"+shortHash(window.settings.ModelSHA256)).Layout),
_ = window.connectionLabel.SetText(window.model.ConnectionText) layout.Rigid(material.Body1(window.theme, "运行依赖:"+window.settings.RuntimeSummary).Layout),
_ = window.stateLabel.SetText("检测状态:正常") layout.Rigid(material.Body1(window.theme, "事件参数:"+window.settings.EventSummary).Layout),
layout.Rigid(material.Body2(window.theme, "参数在配置文件中维护;修改后请停止并重新开始监控。").Layout),
)
}) })
} }
func (window *Window) layoutDialog(gtx layout.Context, record alert.Record) layout.Dimensions {
if window.ackButton.Clicked(gtx) {
window.mu.Lock()
window.pending = nil
window.mu.Unlock()
}
width := gtx.Dp(420)
if width > gtx.Constraints.Max.X {
width = gtx.Constraints.Max.X
}
gtx.Constraints.Min.X = width
gtx.Constraints.Max.X = width
return layout.Stack{}.Layout(gtx,
layout.Expanded(func(gtx layout.Context) layout.Dimensions {
rect := image.Rectangle{Max: gtx.Constraints.Min}
paint.FillShape(gtx.Ops, colorSurface, clip.UniformRRect(rect, gtx.Dp(8)).Op(gtx.Ops))
return layout.Dimensions{Size: gtx.Constraints.Min}
}),
layout.Stacked(func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(16)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return layout.Flex{Axis: layout.Vertical}.Layout(gtx,
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
title := material.H6(window.theme, "确认摔倒")
title.Color = colorAlert
return title.Layout(gtx)
}),
layout.Rigid(material.Body1(window.theme, "人员:"+record.Event.TrackID).Layout),
layout.Rigid(material.Body1(window.theme, fmt.Sprintf("延迟:%.2f 秒", record.Event.LatencySeconds)).Layout),
layout.Rigid(material.Body2(window.theme, "截图:"+record.ScreenshotPath).Layout),
layout.Rigid(layout.Spacer{Height: unit.Dp(8)}.Layout),
layout.Rigid(material.Button(window.theme, &window.ackButton, "我已知晓").Layout),
)
})
}),
)
}
func highestState(result fall.FrameResult) fall.State { func highestState(result fall.FrameResult) fall.State {
state := fall.Normal state := fall.Normal
for _, person := range result.People { for _, person := range result.People {