Skip to content

Commit 51ef84b

Browse files
committed
feat: split window positioning logic to Core/ and add tests for it
1 parent b8d8954 commit 51ef84b

File tree

5 files changed

+216
-48
lines changed

5 files changed

+216
-48
lines changed
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
public enum WindowPositioning {
2+
public struct Point: Equatable {
3+
public var x: Double
4+
public var y: Double
5+
6+
public init(x: Double, y: Double) {
7+
self.x = x
8+
self.y = y
9+
}
10+
}
11+
12+
public struct Size: Equatable {
13+
public var width: Double
14+
public var height: Double
15+
16+
public init(width: Double, height: Double) {
17+
self.width = width
18+
self.height = height
19+
}
20+
}
21+
22+
public struct Rect: Equatable {
23+
public var origin: Point
24+
public var size: Size
25+
26+
public init(origin: Point, size: Size) {
27+
self.origin = origin
28+
self.size = size
29+
}
30+
31+
public var minX: Double {
32+
origin.x
33+
}
34+
public var minY: Double {
35+
origin.y
36+
}
37+
public var maxX: Double {
38+
origin.x + size.width
39+
}
40+
public var maxY: Double {
41+
origin.y + size.height
42+
}
43+
public var width: Double {
44+
size.width
45+
}
46+
public var height: Double {
47+
size.height
48+
}
49+
}
50+
51+
public static func frameNearCursor(
52+
currentFrame: Rect,
53+
screenRect: Rect,
54+
cursorLocation: Point,
55+
desiredSize: Size,
56+
cursorHeight: Double = 16
57+
) -> Rect {
58+
var newWindowFrame = currentFrame
59+
newWindowFrame.size = desiredSize
60+
61+
let cursorY = cursorLocation.y
62+
if cursorY - desiredSize.height < screenRect.origin.y {
63+
newWindowFrame.origin = Point(x: cursorLocation.x, y: cursorLocation.y + cursorHeight)
64+
} else {
65+
newWindowFrame.origin = Point(x: cursorLocation.x, y: cursorLocation.y - desiredSize.height - cursorHeight)
66+
}
67+
68+
if newWindowFrame.maxX > screenRect.maxX {
69+
newWindowFrame.origin.x = screenRect.maxX - newWindowFrame.width
70+
}
71+
return newWindowFrame
72+
}
73+
74+
public static func frameRightOfAnchor(
75+
currentFrame: Rect,
76+
anchorFrame: Rect,
77+
screenRect: Rect,
78+
gap: Double = 8
79+
) -> Rect {
80+
var frame = currentFrame
81+
frame.origin.x = anchorFrame.maxX + gap
82+
frame.origin.y = anchorFrame.origin.y
83+
84+
if frame.minX < screenRect.minX {
85+
frame.origin.x = screenRect.minX
86+
} else if frame.maxX > screenRect.maxX {
87+
frame.origin.x = screenRect.maxX - frame.width
88+
}
89+
90+
if frame.minY < screenRect.minY {
91+
frame.origin.y = screenRect.minY
92+
} else if frame.maxY > screenRect.maxY {
93+
frame.origin.y = screenRect.maxY - frame.height
94+
}
95+
96+
return frame
97+
}
98+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import Core
2+
import Testing
3+
4+
@Test func testFrameNearCursorPlacesBelowWhenNotEnoughSpace() async throws {
5+
let currentFrame = WindowPositioning.Rect(
6+
origin: .init(x: 0, y: 0),
7+
size: .init(width: 40, height: 20)
8+
)
9+
let screenRect = WindowPositioning.Rect(
10+
origin: .init(x: 0, y: 0),
11+
size: .init(width: 100, height: 100)
12+
)
13+
let cursorLocation = WindowPositioning.Point(x: 50, y: 10)
14+
let desiredSize = WindowPositioning.Size(width: 40, height: 30)
15+
16+
let frame = WindowPositioning.frameNearCursor(
17+
currentFrame: currentFrame,
18+
screenRect: screenRect,
19+
cursorLocation: cursorLocation,
20+
desiredSize: desiredSize
21+
)
22+
23+
#expect(frame.origin == WindowPositioning.Point(x: 50, y: 26))
24+
#expect(frame.size == desiredSize)
25+
}
26+
27+
@Test func testFrameNearCursorAdjustsRightEdge() async throws {
28+
let currentFrame = WindowPositioning.Rect(
29+
origin: .init(x: 0, y: 0),
30+
size: .init(width: 20, height: 20)
31+
)
32+
let screenRect = WindowPositioning.Rect(
33+
origin: .init(x: 0, y: 0),
34+
size: .init(width: 100, height: 100)
35+
)
36+
let cursorLocation = WindowPositioning.Point(x: 95, y: 50)
37+
let desiredSize = WindowPositioning.Size(width: 20, height: 20)
38+
39+
let frame = WindowPositioning.frameNearCursor(
40+
currentFrame: currentFrame,
41+
screenRect: screenRect,
42+
cursorLocation: cursorLocation,
43+
desiredSize: desiredSize
44+
)
45+
46+
#expect(frame.origin == WindowPositioning.Point(x: 80, y: 14))
47+
}
48+
49+
@Test func testFrameRightOfAnchorClampsToVisibleFrame() async throws {
50+
let currentFrame = WindowPositioning.Rect(
51+
origin: .init(x: 0, y: 0),
52+
size: .init(width: 30, height: 20)
53+
)
54+
let screenRect = WindowPositioning.Rect(
55+
origin: .init(x: 0, y: 0),
56+
size: .init(width: 100, height: 100)
57+
)
58+
let anchorFrame = WindowPositioning.Rect(
59+
origin: .init(x: 80, y: 10),
60+
size: .init(width: 30, height: 20)
61+
)
62+
63+
let frame = WindowPositioning.frameRightOfAnchor(
64+
currentFrame: currentFrame,
65+
anchorFrame: anchorFrame,
66+
screenRect: screenRect,
67+
gap: 8
68+
)
69+
70+
#expect(frame.origin == WindowPositioning.Point(x: 70, y: 10))
71+
#expect(frame.size == currentFrame.size)
72+
}

azooKeyMac/InputController/CandidateWindow/BaseCandidateViewController.swift

Lines changed: 7 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import Cocoa
2+
import Core
23
import KanaKanjiConverterModule
34

45
class NonClickableTableView: NSTableView {
@@ -160,28 +161,6 @@ class BaseCandidateViewController: NSViewController {
160161
}
161162
}
162163

163-
func getNewWindowFrame(currentFrame: NSRect, screenRect: NSRect, cursorLocation: CGPoint, desiredSize: CGSize, cursorHeight: CGFloat = 16) -> NSRect {
164-
var newWindowFrame = currentFrame
165-
newWindowFrame.size = desiredSize
166-
167-
// 画面のサイズを取得
168-
let cursorY = cursorLocation.y
169-
170-
// カーソルの高さを考慮してウィンドウ位置を調整
171-
// ウィンドウをカーソルの下に表示
172-
if cursorY - desiredSize.height < screenRect.origin.y {
173-
newWindowFrame.origin = CGPoint(x: cursorLocation.x, y: cursorLocation.y + cursorHeight)
174-
} else {
175-
newWindowFrame.origin = CGPoint(x: cursorLocation.x, y: cursorLocation.y - desiredSize.height - cursorHeight)
176-
}
177-
178-
// 右端でウィンドウが画面外に出る場合は左にシフト
179-
if newWindowFrame.maxX > screenRect.maxX {
180-
newWindowFrame.origin.x = screenRect.maxX - newWindowFrame.width
181-
}
182-
return newWindowFrame
183-
}
184-
185164
func getMaxTextWidth(candidates: some Sequence<String>, font: NSFont = .systemFont(ofSize: 18)) -> CGFloat {
186165
candidates.reduce(0) { maxWidth, candidate in
187166
let attributedString = NSAttributedString(
@@ -214,13 +193,12 @@ class BaseCandidateViewController: NSViewController {
214193

215194
let maxWidth = self.getMaxTextWidth(candidates: self.candidates.lazy.map { $0.text })
216195
let windowWidth = self.getWindowWidth(maxContentWidth: maxWidth)
217-
218-
let newWindowFrame = self.getNewWindowFrame(
219-
currentFrame: window.frame,
220-
screenRect: screen.visibleFrame,
221-
cursorLocation: cursorLocation,
222-
desiredSize: CGSize(width: windowWidth, height: tableViewHeight)
223-
)
196+
let newWindowFrame = WindowPositioning.frameNearCursor(
197+
currentFrame: .init(window.frame),
198+
screenRect: .init(screen.visibleFrame),
199+
cursorLocation: .init(cursorLocation),
200+
desiredSize: .init(width: windowWidth, height: tableViewHeight),
201+
).cgRect
224202
if newWindowFrame != window.frame {
225203
window.setFrame(newWindowFrame, display: true, animate: false)
226204
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import Core
2+
import CoreGraphics
3+
4+
extension WindowPositioning.Point {
5+
init(_ point: CGPoint) {
6+
self.init(x: Double(point.x), y: Double(point.y))
7+
}
8+
9+
var cgPoint: CGPoint {
10+
CGPoint(x: CGFloat(x), y: CGFloat(y))
11+
}
12+
}
13+
14+
extension WindowPositioning.Size {
15+
init(_ size: CGSize) {
16+
self.init(width: Double(size.width), height: Double(size.height))
17+
}
18+
19+
var cgSize: CGSize {
20+
CGSize(width: CGFloat(width), height: CGFloat(height))
21+
}
22+
}
23+
24+
extension WindowPositioning.Rect {
25+
init(_ rect: CGRect) {
26+
self.init(origin: WindowPositioning.Point(rect.origin), size: WindowPositioning.Size(rect.size))
27+
}
28+
29+
var cgRect: CGRect {
30+
CGRect(origin: origin.cgPoint, size: size.cgSize)
31+
}
32+
}

azooKeyMac/InputController/azooKeyMacInputController.swift

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -579,25 +579,13 @@ class azooKeyMacInputController: IMKInputController, NSMenuItemValidation { // s
579579
return
580580
}
581581

582-
let anchorFrame = self.candidatesWindow.frame
583-
var frame = self.predictionWindow.frame
584-
frame.origin.x = anchorFrame.maxX + gap
585-
frame.origin.y = anchorFrame.origin.y
586-
587-
let visibleFrame = screen.visibleFrame
588-
if frame.minX < visibleFrame.minX {
589-
frame.origin.x = visibleFrame.minX
590-
} else if frame.maxX > visibleFrame.maxX {
591-
frame.origin.x = visibleFrame.maxX - frame.width
592-
}
593-
594-
if frame.minY < visibleFrame.minY {
595-
frame.origin.y = visibleFrame.minY
596-
} else if frame.maxY > visibleFrame.maxY {
597-
frame.origin.y = visibleFrame.maxY - frame.height
598-
}
599-
600-
self.predictionWindow.setFrame(frame, display: true)
582+
let frame = WindowPositioning.frameRightOfAnchor(
583+
currentFrame: WindowPositioning.Rect(self.predictionWindow.frame),
584+
anchorFrame: WindowPositioning.Rect(self.candidatesWindow.frame),
585+
screenRect: WindowPositioning.Rect(screen.visibleFrame),
586+
gap: Double(gap)
587+
)
588+
self.predictionWindow.setFrame(frame.cgRect, display: true)
601589
}
602590

603591
private func showCachedPredictionWindow() {

0 commit comments

Comments
 (0)