1package daemon
2
3import (
4 "github.com/bnema/vev/internal/domain"
5 scopy "github.com/bnema/vev/internal/usecase/copy"
6 "github.com/bnema/vev/internal/usecase/mouse"
7)
8
9// clientTopBarRows is deliberately kept at the client/frame boundary. Layout
10// coordinates are content-relative and must cross this boundary exactly once.
11const clientTopBarRows = 1
12
13type copyMouseGeometry struct {
14 pane *pane
15 content domain.Rect // absolute client-frame coordinates
16}
17
18type mappedCopyMouse struct {
19 pane *pane
20 pos scopy.Pos
21}
22
23func copyContentToClientRect(content domain.Rect) domain.Rect {
24 content.Y += clientTopBarRows
25 return content
26}
27
28func mapCopyMouse(ev mouse.Event, geometry copyMouseGeometry, viewportTop int, document *scopy.Document, clamp bool) (mappedCopyMouse, bool) {
29 if document == nil || geometry.pane == nil || geometry.content.Width <= 0 || geometry.content.Height <= 0 {
30 return mappedCopyMouse{}, false
31 }
32 col, row := ev.Col, ev.Row
33 if clamp {
34 col = clampInt(col, geometry.content.X, geometry.content.X+geometry.content.Width-1)
35 row = clampInt(row, geometry.content.Y, geometry.content.Y+geometry.content.Height-1)
36 } else if !pointInRect(col, row, geometry.content) {
37 return mappedCopyMouse{}, false
38 }
39 pos, ok := document.Normalize(scopy.Pos{Row: viewportTop + row - geometry.content.Y, Col: col - geometry.content.X})
40 if !ok {
41 return mappedCopyMouse{}, false
42 }
43 return mappedCopyMouse{pane: geometry.pane, pos: pos}, true
44}
45
46// hitTestCopyMouseGeometryLocked resolves a new press. Floating geometry is
47// exclusive while visible; normal title bars and dividers are intentionally
48// not copy content. Caller holds tb.mu.
49func hitTestCopyMouseGeometryLocked(tb *tab, cfg domain.FloatingConfig, col, row int) (copyMouseGeometry, bool) {
50 if tb == nil || row < clientTopBarRows {
51 return copyMouseGeometry{}, false
52 }
53 if p, geometry, visible := tb.visibleFloatingSnapshotLocked(cfg); visible {
54 content := copyContentToClientRect(geometry.Inner)
55 if !pointInRect(col, row, content) {
56 return copyMouseGeometry{}, false
57 }
58 return copyMouseGeometry{pane: p, content: content}, true
59 }
60 pl, ok := hitTestPlacementLocked(tb, col, row-clientTopBarRows)
61 if !ok || pl.Collapsed || !pointInRect(col, row-clientTopBarRows, pl.Content) {
62 return copyMouseGeometry{}, false
63 }
64 p := tb.panes[pl.ID]
65 if p == nil {
66 return copyMouseGeometry{}, false
67 }
68 return copyMouseGeometry{pane: p, content: copyContentToClientRect(pl.Content)}, true
69}
70
71// copyMouseGeometryForPaneLocked resolves geometry for an existing drag. It
72// never hit-tests the new pointer, preventing cross-pane document switches.
73func copyMouseGeometryForPaneLocked(tb *tab, cfg domain.FloatingConfig, target *pane) (copyMouseGeometry, bool) {
74 if tb == nil || target == nil {
75 return copyMouseGeometry{}, false
76 }
77 if p, geometry, visible := tb.visibleFloatingSnapshotLocked(cfg); visible {
78 if p != target {
79 return copyMouseGeometry{}, false
80 }
81 return copyMouseGeometry{pane: p, content: copyContentToClientRect(geometry.Inner)}, true
82 }
83 if tb.panes[target.id] != target {
84 return copyMouseGeometry{}, false
85 }
86 placements, ok := solvedPlacementsLocked(tb)
87 if !ok {
88 return copyMouseGeometry{}, false
89 }
90 for _, pl := range placements {
91 if pl.ID == target.id && !pl.Collapsed && pl.Content.Width > 0 && pl.Content.Height > 0 {
92 return copyMouseGeometry{pane: target, content: copyContentToClientRect(pl.Content)}, true
93 }
94 }
95 return copyMouseGeometry{}, false
96}