first commit
This commit is contained in:
commit
3553790475
27 files changed
+3839
No files matched your search
@@ -0,0 +1,299 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"time"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/canvas"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/driver/desktop"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"viewsagent/internal/capture"
|
||||
"viewsagent/internal/win32"
|
||||
)
|
||||
|
||||
// overlayTitle 是框选窗口标题,win32 需要靠它找到 HWND。
|
||||
const overlayTitle = "viewsagent-overlay"
|
||||
|
||||
// overlay 是一个铺满当前显示器的冻结帧框选层。
|
||||
//
|
||||
// 之所以不依赖窗口透明:Windows 下的 OpenGL 窗口透明并不可靠,
|
||||
// 直接用"全屏截图做背景 + 遮罩"视觉等效,还能保证选区与截图完全对齐。
|
||||
type overlay struct {
|
||||
win fyne.Window
|
||||
|
||||
vd win32.Rect
|
||||
mapping capture.Mapping
|
||||
size fyne.Size
|
||||
calibrated bool
|
||||
|
||||
bg *canvas.Image
|
||||
dims [4]*canvas.Rectangle
|
||||
edges [4]*canvas.Rectangle
|
||||
hint *canvas.Text
|
||||
catcher *catcher
|
||||
|
||||
start, cur fyne.Position
|
||||
dragging bool
|
||||
closed bool
|
||||
|
||||
onDone func(capture.Region)
|
||||
onCancel func()
|
||||
}
|
||||
|
||||
// newOverlay 创建框选层。shot 应该是调用前抓好的整屏画面。
|
||||
func newOverlay(app fyne.App, shot image.Image, vd win32.Rect,
|
||||
onDone func(capture.Region), onCancel func()) *overlay {
|
||||
|
||||
o := &overlay{vd: vd, onDone: onDone, onCancel: onCancel}
|
||||
o.win = app.NewWindow(overlayTitle)
|
||||
o.win.SetPadded(false)
|
||||
o.win.SetFullScreen(true)
|
||||
|
||||
o.bg = canvas.NewImageFromImage(shot)
|
||||
o.bg.FillMode = canvas.ImageFillStretch
|
||||
|
||||
objs := []fyne.CanvasObject{o.bg}
|
||||
for i := range o.dims {
|
||||
o.dims[i] = canvas.NewRectangle(color.NRGBA{R: 0, G: 0, B: 0, A: 130})
|
||||
objs = append(objs, o.dims[i])
|
||||
}
|
||||
for i := range o.edges {
|
||||
o.edges[i] = canvas.NewRectangle(color.NRGBA{R: 0x5A, G: 0xC8, B: 0xFF, A: 0xFF})
|
||||
objs = append(objs, o.edges[i])
|
||||
}
|
||||
o.hint = canvas.NewText("拖拽框选区域 · Esc 取消", color.White)
|
||||
o.hint.TextStyle = fyne.TextStyle{Bold: true}
|
||||
o.hint.TextSize = 15
|
||||
|
||||
o.catcher = newCatcher(o)
|
||||
objs = append(objs, o.catcher, o.hint)
|
||||
|
||||
o.win.SetContent(container.NewWithoutLayout(objs...))
|
||||
o.win.Canvas().SetOnTypedKey(func(k *fyne.KeyEvent) {
|
||||
if k.Name == fyne.KeyEscape {
|
||||
o.cancel()
|
||||
}
|
||||
})
|
||||
return o
|
||||
}
|
||||
|
||||
// show 显示框选层并做一次坐标校准。
|
||||
func (o *overlay) show() {
|
||||
o.win.Show()
|
||||
// 窗口尺寸在 Show 之后才确定,稍等一拍再校准。
|
||||
go func() {
|
||||
time.Sleep(90 * time.Millisecond)
|
||||
fyne.Do(o.calibrate)
|
||||
}()
|
||||
}
|
||||
|
||||
// calibrate 反推"逻辑坐标 → 物理像素"的比例与原点。
|
||||
// 宁可依赖实际窗口尺寸,也不假设系统缩放,这样 125%/150% 屏都不会偏。
|
||||
func (o *overlay) calibrate() {
|
||||
size := o.win.Canvas().Size()
|
||||
if size.Width < 10 || size.Height < 10 {
|
||||
return
|
||||
}
|
||||
o.size = size
|
||||
|
||||
rect, err := windowRectByTitle(overlayTitle)
|
||||
if err != nil {
|
||||
mon, _ := win32.MonitorAtCursor()
|
||||
rect = mon.Rect
|
||||
}
|
||||
if rect.Width() <= 0 || rect.Height() <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
o.mapping = capture.Mapping{
|
||||
OffsetX: float64(rect.Left),
|
||||
OffsetY: float64(rect.Top),
|
||||
ScaleX: float64(rect.Width()) / float64(size.Width),
|
||||
ScaleY: float64(rect.Height()) / float64(size.Height),
|
||||
}
|
||||
if o.mapping.ScaleX <= 0 || o.mapping.ScaleY <= 0 {
|
||||
o.mapping.ScaleX, o.mapping.ScaleY = 1, 1
|
||||
}
|
||||
|
||||
// 把整张虚拟桌面按当前窗口的实际映射铺到画布上。
|
||||
bx, by, bw, bh := o.mapping.ToLogical(capture.Region{
|
||||
X: o.vd.Left, Y: o.vd.Top, W: o.vd.Width(), H: o.vd.Height(),
|
||||
})
|
||||
o.bg.Move(fyne.NewPos(float32(bx), float32(by)))
|
||||
o.bg.Resize(fyne.NewSize(float32(bw), float32(bh)))
|
||||
|
||||
o.catcher.Move(fyne.NewPos(0, 0))
|
||||
o.catcher.Resize(size)
|
||||
|
||||
o.calibrated = true
|
||||
o.updateSelection()
|
||||
}
|
||||
|
||||
func windowRectByTitle(title string) (win32.Rect, error) {
|
||||
hwnd, err := win32.FindWindowByTitle(title)
|
||||
if err != nil {
|
||||
return win32.Rect{}, err
|
||||
}
|
||||
return win32.WindowRect(hwnd)
|
||||
}
|
||||
|
||||
// selection 返回当前选区(逻辑坐标,已归一化)。
|
||||
func (o *overlay) selection() (x0, y0, x1, y1 float32) {
|
||||
x0, y0 = o.start.X, o.start.Y
|
||||
x1, y1 = o.cur.X, o.cur.Y
|
||||
if x1 < x0 {
|
||||
x0, x1 = x1, x0
|
||||
}
|
||||
if y1 < y0 {
|
||||
y0, y1 = y1, y0
|
||||
}
|
||||
// 限制在窗口内
|
||||
x0 = fyne.Max(x0, 0)
|
||||
y0 = fyne.Max(y0, 0)
|
||||
x1 = fyne.Min(x1, o.size.Width)
|
||||
y1 = fyne.Min(y1, o.size.Height)
|
||||
return
|
||||
}
|
||||
|
||||
// updateSelection 重排遮罩、边框与尺寸提示。
|
||||
func (o *overlay) updateSelection() {
|
||||
if o.size.Width <= 0 {
|
||||
return
|
||||
}
|
||||
w, h := o.size.Width, o.size.Height
|
||||
x0, y0, x1, y1 := o.selection()
|
||||
selW, selH := x1-x0, y1-y0
|
||||
|
||||
if selW < 2 || selH < 2 {
|
||||
// 尚未框选:整屏压暗
|
||||
o.dims[0].Move(fyne.NewPos(0, 0))
|
||||
o.dims[0].Resize(fyne.NewSize(w, h))
|
||||
for i := 1; i < 4; i++ {
|
||||
o.dims[i].Resize(fyne.NewSize(0, 0))
|
||||
}
|
||||
for _, e := range o.edges {
|
||||
e.Resize(fyne.NewSize(0, 0))
|
||||
}
|
||||
o.hint.Text = "拖拽框选区域 · Esc 取消"
|
||||
o.hint.Move(fyne.NewPos(theme.Padding()*2, theme.Padding()*2))
|
||||
o.hint.Refresh()
|
||||
return
|
||||
}
|
||||
|
||||
// 上下左右四块压暗区域,挖出选区
|
||||
o.dims[0].Move(fyne.NewPos(0, 0))
|
||||
o.dims[0].Resize(fyne.NewSize(w, y0))
|
||||
o.dims[1].Move(fyne.NewPos(0, y1))
|
||||
o.dims[1].Resize(fyne.NewSize(w, h-y1))
|
||||
o.dims[2].Move(fyne.NewPos(0, y0))
|
||||
o.dims[2].Resize(fyne.NewSize(x0, selH))
|
||||
o.dims[3].Move(fyne.NewPos(x1, y0))
|
||||
o.dims[3].Resize(fyne.NewSize(w-x1, selH))
|
||||
|
||||
// 选区描边
|
||||
const t = 2
|
||||
o.edges[0].Move(fyne.NewPos(x0, y0))
|
||||
o.edges[0].Resize(fyne.NewSize(selW, t))
|
||||
o.edges[1].Move(fyne.NewPos(x0, y1-t))
|
||||
o.edges[1].Resize(fyne.NewSize(selW, t))
|
||||
o.edges[2].Move(fyne.NewPos(x0, y0))
|
||||
o.edges[2].Resize(fyne.NewSize(t, selH))
|
||||
o.edges[3].Move(fyne.NewPos(x1-t, y0))
|
||||
o.edges[3].Resize(fyne.NewSize(t, selH))
|
||||
|
||||
phys := o.mapping.ToPhysical(float64(x0), float64(y0), float64(selW), float64(selH))
|
||||
o.hint.Text = fmt.Sprintf("%d × %d", phys.W, phys.H)
|
||||
hy := y0 - 26
|
||||
if hy < 4 {
|
||||
hy = y0 + 6
|
||||
}
|
||||
o.hint.Move(fyne.NewPos(x0+6, hy))
|
||||
o.hint.Refresh()
|
||||
}
|
||||
|
||||
// finish 确认选区。
|
||||
func (o *overlay) finish() {
|
||||
if !o.calibrated {
|
||||
o.calibrate()
|
||||
}
|
||||
x0, y0, x1, y1 := o.selection()
|
||||
if x1-x0 < 6 || y1-y0 < 6 {
|
||||
o.cancel()
|
||||
return
|
||||
}
|
||||
r := o.mapping.ToPhysical(float64(x0), float64(y0), float64(x1-x0), float64(y1-y0))
|
||||
if r.W < 8 || r.H < 8 {
|
||||
o.cancel()
|
||||
return
|
||||
}
|
||||
done := o.onDone
|
||||
o.close()
|
||||
if done != nil {
|
||||
done(r)
|
||||
}
|
||||
}
|
||||
|
||||
// cancel 取消框选。
|
||||
func (o *overlay) cancel() {
|
||||
cancel := o.onCancel
|
||||
o.close()
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *overlay) close() {
|
||||
if o.closed {
|
||||
return
|
||||
}
|
||||
o.closed = true
|
||||
o.win.Close()
|
||||
}
|
||||
|
||||
// catcher 是铺满窗口的事件接收层,负责鼠标拖拽。
|
||||
type catcher struct {
|
||||
widget.BaseWidget
|
||||
o *overlay
|
||||
}
|
||||
|
||||
func newCatcher(o *overlay) *catcher {
|
||||
c := &catcher{o: o}
|
||||
c.ExtendBaseWidget(c)
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *catcher) CreateRenderer() fyne.WidgetRenderer {
|
||||
return widget.NewSimpleRenderer(canvas.NewRectangle(color.Transparent))
|
||||
}
|
||||
|
||||
func (c *catcher) Dragged(e *fyne.DragEvent) {
|
||||
if !c.o.dragging {
|
||||
return
|
||||
}
|
||||
c.o.cur = e.Position
|
||||
c.o.updateSelection()
|
||||
}
|
||||
|
||||
func (c *catcher) DragEnd() {}
|
||||
|
||||
func (c *catcher) MouseDown(e *desktop.MouseEvent) {
|
||||
c.o.dragging = true
|
||||
c.o.start = e.Position
|
||||
c.o.cur = e.Position
|
||||
c.o.updateSelection()
|
||||
}
|
||||
|
||||
func (c *catcher) MouseUp(e *desktop.MouseEvent) {
|
||||
if !c.o.dragging {
|
||||
return
|
||||
}
|
||||
c.o.dragging = false
|
||||
c.o.cur = e.Position
|
||||
c.o.finish()
|
||||
}
|
||||
Reference in new issue
Block a user