diff --git a/v2/internal/ui/screen_other.go b/v2/internal/ui/screen_other.go new file mode 100644 index 0000000..8474fb0 --- /dev/null +++ b/v2/internal/ui/screen_other.go @@ -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 } diff --git a/v2/internal/ui/screen_windows.go b/v2/internal/ui/screen_windows.go new file mode 100644 index 0000000..408faa4 --- /dev/null +++ b/v2/internal/ui/screen_windows.go @@ -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) +} diff --git a/v2/internal/ui/window.go b/v2/internal/ui/window.go index 2ac6c8e..1ef2b1b 100644 --- a/v2/internal/ui/window.go +++ b/v2/internal/ui/window.go @@ -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])