feat(v2): T-308A window 80% of screen, 2x3 gallery, Esc close, button states

- Window opens at 80% of the primary display (Windows GetSystemMetrics).
- Gallery is now 2 columns x 3 rows and the right panel is narrower.
- Esc closes the full-image viewer (key focus + filter).
- Start/Stop grey out per running state (gtx.Disabled drops input); a hard
  ReportIssue resets running so Start re-enables. Cross-compiles for Windows.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
ila
2026-07-23 21:16:37 +08:00
co-authored by Claude Opus 4.8
parent 8840bdd115
commit 550e6ad3ca
3 changed files with 84 additions and 17 deletions
+6
View File
@@ -0,0 +1,6 @@
//go:build !windows
package ui
// screenSize is unknown off Windows; the caller falls back to the spec size.
func screenSize() (int, int) { return 0, 0 }
+18
View File
@@ -0,0 +1,18 @@
//go:build windows
package ui
// SM_CXSCREEN / SM_CYSCREEN return the primary display size in pixels.
const (
smCXScreen = 0
smCYScreen = 1
)
var procGetSystemMetrics = user32.NewProc("GetSystemMetrics")
// screenSize returns the primary display size in pixels, or (0, 0) if unavailable.
func screenSize() (int, int) {
width, _, _ := procGetSystemMetrics.Call(uintptr(smCXScreen))
height, _, _ := procGetSystemMetrics.Call(uintptr(smCYScreen))
return int(width), int(height)
}
+60 -17
View File
@@ -12,6 +12,7 @@ import (
"gioui.org/app"
"gioui.org/font/gofont"
"gioui.org/io/key"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/op/clip"
@@ -26,7 +27,11 @@ import (
"silverpose/v2/internal/monitor"
)
const galleryCapacity = 9 // 3 columns x 3 rows
const (
galleryColumns = 2
galleryRows = 3
galleryCapacity = galleryColumns * galleryRows // 2 columns x 3 rows
)
// Light Windows theme tokens (see docs/ui/silver-pose-ui-ux-spec.md).
var (
@@ -83,6 +88,7 @@ type Window struct {
seenAlerts map[string]struct{}
incoming []galleryIncoming
seenEvents map[string]struct{}
running bool
// Touched only by the event-loop goroutine.
imageOp paint.ImageOp
@@ -110,10 +116,15 @@ func NewWindow(settings Settings, commands Commands) (*Window, error) {
model := NewViewModel()
model.SetSourceReady(settings.SourceEnvironment, settings.SourceEnvironment != "")
width, height := spec.Width, spec.Height
if screenWidth, screenHeight := screenSize(); screenWidth > 0 && screenHeight > 0 {
width = screenWidth * 80 / 100
height = screenHeight * 80 / 100
}
appWindow := new(app.Window)
appWindow.Option(
app.Title(spec.Title),
app.Size(unit.Dp(spec.Width), unit.Dp(spec.Height)),
app.Size(unit.Dp(width), unit.Dp(height)),
app.MinSize(unit.Dp(960), unit.Dp(640)),
)
window := &Window{
@@ -159,12 +170,18 @@ func (window *Window) loop() error {
}
func (window *Window) start() {
window.mu.Lock()
window.running = true
window.mu.Unlock()
if window.commands.Start != nil {
window.commands.Start()
}
}
func (window *Window) stop() {
window.mu.Lock()
window.running = false
window.mu.Unlock()
if window.commands.Stop != nil {
window.commands.Stop()
}
@@ -227,6 +244,7 @@ func (window *Window) ReportIssue(message string) {
window.model.ConnectionText = "视频流:" + message
window.model.Critical = false
window.model.StateText = "检测状态:正常"
window.running = false
window.mu.Unlock()
window.appWindow.Invalidate()
}
@@ -240,6 +258,7 @@ func (window *Window) layout(gtx layout.Context) layout.Dimensions {
gen := window.imageGen
pending := window.pending
incoming := window.incoming
running := window.running
window.incoming = nil
window.mu.Unlock()
@@ -269,6 +288,19 @@ func (window *Window) layout(gtx layout.Context) layout.Dimensions {
if window.viewerClose.Clicked(gtx) {
window.viewing = nil
}
// Esc closes the full-image viewer.
for {
event, ok := gtx.Event(key.Filter{Focus: window, Name: key.NameEscape})
if !ok {
break
}
if pressed, ok := event.(key.Event); ok && pressed.State == key.Press {
window.viewing = nil
}
}
if window.viewing != nil {
gtx.Execute(key.FocusCmd{Tag: window})
}
if window.ackButton.Clicked(gtx) {
window.mu.Lock()
window.pending = nil
@@ -281,7 +313,7 @@ func (window *Window) layout(gtx layout.Context) layout.Dimensions {
layout.Rigid(window.layoutTabs),
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if window.active == 0 {
return window.layoutMonitor(gtx, model, hasFrame)
return window.layoutMonitor(gtx, model, hasFrame, running)
}
return window.layoutSettings(gtx, model)
}),
@@ -335,9 +367,9 @@ func (window *Window) layoutTabs(gtx layout.Context) layout.Dimensions {
return layout.Flex{}.Layout(gtx, children...)
}
func (window *Window) layoutMonitor(gtx layout.Context, model ViewModel, hasFrame bool) layout.Dimensions {
func (window *Window) layoutMonitor(gtx layout.Context, model ViewModel, hasFrame, running bool) layout.Dimensions {
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
layout.Flexed(3, func(gtx layout.Context) layout.Dimensions {
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)
@@ -345,17 +377,17 @@ func (window *Window) layoutMonitor(gtx layout.Context, model ViewModel, hasFram
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.layoutRightPanel(gtx, model)
layout.Flexed(3, func(gtx layout.Context) layout.Dimensions {
return window.layoutRightPanel(gtx, model, running)
}),
)
}
func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel) layout.Dimensions {
if window.startButton.Clicked(gtx) {
func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel, running bool) layout.Dimensions {
if !running && window.startButton.Clicked(gtx) {
window.start()
}
if window.stopButton.Clicked(gtx) {
if running && window.stopButton.Clicked(gtx) {
window.stop()
}
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
@@ -379,11 +411,11 @@ func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel) layo
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 smallButton(gtx, window.theme, &window.startButton, "开始监控")
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 smallButton(gtx, window.theme, &window.stopButton, "停止监控")
return stateButton(gtx, window.theme, &window.stopButton, "停止监控", running)
}),
)
}),
@@ -410,20 +442,31 @@ func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel) layo
}
func smallButton(gtx layout.Context, theme *material.Theme, click *widget.Clickable, label string) layout.Dimensions {
return stateButton(gtx, theme, click, label, true)
}
// stateButton renders a compact button; when disabled it is greyed and, via
// gtx.Disabled(), drops pointer input so the click never registers.
func stateButton(gtx layout.Context, theme *material.Theme, click *widget.Clickable, label string, enabled bool) layout.Dimensions {
button := material.Button(theme, click, label)
button.Inset = layout.Inset{Top: unit.Dp(4), Bottom: unit.Dp(4), Left: unit.Dp(10), Right: unit.Dp(10)}
button.TextSize = unit.Sp(13)
if !enabled {
button.Background = color.NRGBA{R: 0xCF, G: 0xD8, B: 0xE3, A: 0xFF}
button.Color = color.NRGBA{R: 0x94, G: 0xA3, B: 0xB8, A: 0xFF}
gtx = gtx.Disabled()
}
return button.Layout(gtx)
}
func (window *Window) layoutGallery(gtx layout.Context) layout.Dimensions {
rows := make([]layout.FlexChild, 0, 3)
for r := 0; r < 3; r++ {
rows := make([]layout.FlexChild, 0, galleryRows)
for r := 0; r < galleryRows; r++ {
r := r
rows = append(rows, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
cols := make([]layout.FlexChild, 0, 3)
for c := 0; c < 3; c++ {
index := r*3 + c
cols := make([]layout.FlexChild, 0, galleryColumns)
for c := 0; c < galleryColumns; c++ {
index := r*galleryColumns + c
cols = append(cols, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
if index < len(window.gallery) {
return window.layoutCell(gtx, window.gallery[index])