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:
@@ -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 }
|
||||||
@@ -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
@@ -12,6 +12,7 @@ import (
|
|||||||
|
|
||||||
"gioui.org/app"
|
"gioui.org/app"
|
||||||
"gioui.org/font/gofont"
|
"gioui.org/font/gofont"
|
||||||
|
"gioui.org/io/key"
|
||||||
"gioui.org/layout"
|
"gioui.org/layout"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
"gioui.org/op/clip"
|
"gioui.org/op/clip"
|
||||||
@@ -26,7 +27,11 @@ import (
|
|||||||
"silverpose/v2/internal/monitor"
|
"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).
|
// Light Windows theme tokens (see docs/ui/silver-pose-ui-ux-spec.md).
|
||||||
var (
|
var (
|
||||||
@@ -83,6 +88,7 @@ type Window struct {
|
|||||||
seenAlerts map[string]struct{}
|
seenAlerts map[string]struct{}
|
||||||
incoming []galleryIncoming
|
incoming []galleryIncoming
|
||||||
seenEvents map[string]struct{}
|
seenEvents map[string]struct{}
|
||||||
|
running bool
|
||||||
|
|
||||||
// Touched only by the event-loop goroutine.
|
// Touched only by the event-loop goroutine.
|
||||||
imageOp paint.ImageOp
|
imageOp paint.ImageOp
|
||||||
@@ -110,10 +116,15 @@ func NewWindow(settings Settings, commands Commands) (*Window, error) {
|
|||||||
model := NewViewModel()
|
model := NewViewModel()
|
||||||
model.SetSourceReady(settings.SourceEnvironment, settings.SourceEnvironment != "")
|
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 := new(app.Window)
|
||||||
appWindow.Option(
|
appWindow.Option(
|
||||||
app.Title(spec.Title),
|
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)),
|
app.MinSize(unit.Dp(960), unit.Dp(640)),
|
||||||
)
|
)
|
||||||
window := &Window{
|
window := &Window{
|
||||||
@@ -159,12 +170,18 @@ func (window *Window) loop() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) start() {
|
func (window *Window) start() {
|
||||||
|
window.mu.Lock()
|
||||||
|
window.running = true
|
||||||
|
window.mu.Unlock()
|
||||||
if window.commands.Start != nil {
|
if window.commands.Start != nil {
|
||||||
window.commands.Start()
|
window.commands.Start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) stop() {
|
func (window *Window) stop() {
|
||||||
|
window.mu.Lock()
|
||||||
|
window.running = false
|
||||||
|
window.mu.Unlock()
|
||||||
if window.commands.Stop != nil {
|
if window.commands.Stop != nil {
|
||||||
window.commands.Stop()
|
window.commands.Stop()
|
||||||
}
|
}
|
||||||
@@ -227,6 +244,7 @@ func (window *Window) ReportIssue(message string) {
|
|||||||
window.model.ConnectionText = "视频流:" + message
|
window.model.ConnectionText = "视频流:" + message
|
||||||
window.model.Critical = false
|
window.model.Critical = false
|
||||||
window.model.StateText = "检测状态:正常"
|
window.model.StateText = "检测状态:正常"
|
||||||
|
window.running = false
|
||||||
window.mu.Unlock()
|
window.mu.Unlock()
|
||||||
window.appWindow.Invalidate()
|
window.appWindow.Invalidate()
|
||||||
}
|
}
|
||||||
@@ -240,6 +258,7 @@ func (window *Window) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
gen := window.imageGen
|
gen := window.imageGen
|
||||||
pending := window.pending
|
pending := window.pending
|
||||||
incoming := window.incoming
|
incoming := window.incoming
|
||||||
|
running := window.running
|
||||||
window.incoming = nil
|
window.incoming = nil
|
||||||
window.mu.Unlock()
|
window.mu.Unlock()
|
||||||
|
|
||||||
@@ -269,6 +288,19 @@ func (window *Window) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
if window.viewerClose.Clicked(gtx) {
|
if window.viewerClose.Clicked(gtx) {
|
||||||
window.viewing = nil
|
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) {
|
if window.ackButton.Clicked(gtx) {
|
||||||
window.mu.Lock()
|
window.mu.Lock()
|
||||||
window.pending = nil
|
window.pending = nil
|
||||||
@@ -281,7 +313,7 @@ func (window *Window) layout(gtx layout.Context) layout.Dimensions {
|
|||||||
layout.Rigid(window.layoutTabs),
|
layout.Rigid(window.layoutTabs),
|
||||||
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
if window.active == 0 {
|
if window.active == 0 {
|
||||||
return window.layoutMonitor(gtx, model, hasFrame)
|
return window.layoutMonitor(gtx, model, hasFrame, running)
|
||||||
}
|
}
|
||||||
return window.layoutSettings(gtx, model)
|
return window.layoutSettings(gtx, model)
|
||||||
}),
|
}),
|
||||||
@@ -335,9 +367,9 @@ func (window *Window) layoutTabs(gtx layout.Context) layout.Dimensions {
|
|||||||
return layout.Flex{}.Layout(gtx, children...)
|
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,
|
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 {
|
return layout.UniformInset(unit.Dp(8)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
||||||
if !hasFrame {
|
if !hasFrame {
|
||||||
return layout.Center.Layout(gtx, material.Body1(window.theme, model.ConnectionText).Layout)
|
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)
|
return widget.Image{Src: window.imageOp, Fit: widget.Contain, Position: layout.Center}.Layout(gtx)
|
||||||
})
|
})
|
||||||
}),
|
}),
|
||||||
layout.Flexed(2, func(gtx layout.Context) layout.Dimensions {
|
layout.Flexed(3, func(gtx layout.Context) layout.Dimensions {
|
||||||
return window.layoutRightPanel(gtx, model)
|
return window.layoutRightPanel(gtx, model, running)
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel) layout.Dimensions {
|
func (window *Window) layoutRightPanel(gtx layout.Context, model ViewModel, running bool) layout.Dimensions {
|
||||||
if window.startButton.Clicked(gtx) {
|
if !running && window.startButton.Clicked(gtx) {
|
||||||
window.start()
|
window.start()
|
||||||
}
|
}
|
||||||
if window.stopButton.Clicked(gtx) {
|
if running && window.stopButton.Clicked(gtx) {
|
||||||
window.stop()
|
window.stop()
|
||||||
}
|
}
|
||||||
return layout.UniformInset(unit.Dp(10)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
|
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 {
|
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
||||||
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
return layout.Flex{Axis: layout.Horizontal}.Layout(gtx,
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
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(layout.Spacer{Width: unit.Dp(8)}.Layout),
|
||||||
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
|
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 {
|
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 := 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.Inset = layout.Inset{Top: unit.Dp(4), Bottom: unit.Dp(4), Left: unit.Dp(10), Right: unit.Dp(10)}
|
||||||
button.TextSize = unit.Sp(13)
|
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)
|
return button.Layout(gtx)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (window *Window) layoutGallery(gtx layout.Context) layout.Dimensions {
|
func (window *Window) layoutGallery(gtx layout.Context) layout.Dimensions {
|
||||||
rows := make([]layout.FlexChild, 0, 3)
|
rows := make([]layout.FlexChild, 0, galleryRows)
|
||||||
for r := 0; r < 3; r++ {
|
for r := 0; r < galleryRows; r++ {
|
||||||
r := r
|
r := r
|
||||||
rows = append(rows, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
rows = append(rows, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
cols := make([]layout.FlexChild, 0, 3)
|
cols := make([]layout.FlexChild, 0, galleryColumns)
|
||||||
for c := 0; c < 3; c++ {
|
for c := 0; c < galleryColumns; c++ {
|
||||||
index := r*3 + c
|
index := r*galleryColumns + c
|
||||||
cols = append(cols, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
cols = append(cols, layout.Flexed(1, func(gtx layout.Context) layout.Dimensions {
|
||||||
if index < len(window.gallery) {
|
if index < len(window.gallery) {
|
||||||
return window.layoutCell(gtx, window.gallery[index])
|
return window.layoutCell(gtx, window.gallery[index])
|
||||||
|
|||||||
Reference in New Issue
Block a user