Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
205 changes: 148 additions & 57 deletions sources/SquirrelPanel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,16 @@ final class SquirrelPanel: NSPanel {
// text done!
view.textView.textContentStorage?.attributedString = text
view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal)

// 強制 TextKit 2 立即同步佈局,確保後續計算窗口和高亮背景時,拿到的是折行後的真實尺寸
let textWidth = maxTextWidth()
let maxTextHeight = vertical ? screenRect.width - theme.edgeInset.width * 2 : screenRect.height - theme.edgeInset.height * 2
view.textContainer.size = NSSize(width: textWidth, height: maxTextHeight)
view.textLayoutManager.ensureLayout(for: view.textLayoutManager.documentRange)

// 重置 NSTextView 內部視圖滾動位置,防止因爲折行超高導致自動滾動到文末(第一行溢出)
view.textView.scrollToBeginningOfDocument(nil)

view.drawView(candidateRanges: candidateRanges, hilightedIndex: index, preeditRange: preeditRange, highlightedPreeditRange: highlightedPreeditRange, canPageUp: page > 0, canPageDown: !lastPage)
show()
}
Expand Down Expand Up @@ -346,8 +356,6 @@ private extension SquirrelPanel {
return maxWidth
}

// Get the window size, the windows will be the dirtyRect in
// SquirrelView.drawRect
// swiftlint:disable:next cyclomatic_complexity
func show() {
currentScreen()
Expand All @@ -359,93 +367,176 @@ private extension SquirrelPanel {
self.appearance = NSAppearance(named: .aqua)
}

// Break line if the text is too long, based on screen size.
let textWidth = maxTextWidth()
let maxTextHeight = vertical ? screenRect.width - theme.edgeInset.width * 2 : screenRect.height - theme.edgeInset.height * 2
view.textContainer.size = NSSize(width: textWidth, height: maxTextHeight)
view.textView.textContainerInset = theme.edgeInset

var textWidth = maxTextWidth()
// 高度設置爲無窮大,放開限制,讓超大文本完全測量出真實自然高度
view.textContainer.size = NSSize(width: textWidth, height: .greatestFiniteMagnitude)

// 嚴禁 NSTextView 自動把 textContainer 縮小到當前的視圖寬度,防止死循環與文字消失
view.textContainer.widthTracksTextView = false
view.textContainer.heightTracksTextView = false

// 強制完成排版,並歸零 bounds
view.textLayoutManager.ensureLayout(for: view.textLayoutManager.documentRange)
view.textView.bounds.origin = .zero

var panelRect = NSRect.zero
// in vertical mode, the width and height are interchanged
var contentRect = view.contentRect
if theme.memorizeSize && (vertical && position.midY / screenRect.height < 0.5) ||
(vertical && position.minX + max(contentRect.width, maxHeight) + theme.edgeInset.width * 2 > screenRect.maxX) {
if contentRect.width >= maxHeight {
maxHeight = contentRect.width
} else {
contentRect.size.width = maxHeight
view.textContainer.size = NSSize(width: maxHeight, height: maxTextHeight)
}
}

// 計算出「不加限制時」需要的自然面板巨型尺寸
var naturalPanelSize = NSSize.zero
if vertical {
panelRect.size = NSSize(width: min(0.95 * screenRect.width, contentRect.height + theme.edgeInset.height * 2),
height: min(0.95 * screenRect.height, contentRect.width + theme.edgeInset.width * 2) + theme.pagingOffset)
naturalPanelSize.width = contentRect.height + theme.edgeInset.height * 2
naturalPanelSize.height = contentRect.width + theme.edgeInset.width * 2 + theme.pagingOffset
} else {
naturalPanelSize.width = contentRect.width + theme.edgeInset.width * 2 + theme.pagingOffset
naturalPanelSize.height = contentRect.height + theme.edgeInset.height * 2
}

// 屏幕最大可用範圍(留白 5%)
let maxAllowedWidth = screenRect.width * 0.95
let maxAllowedHeight = screenRect.height * 0.95

// 判斷是否需要觸發「全屏模式」
let requiresFullScreen = naturalPanelSize.width > maxAllowedWidth || naturalPanelSize.height > maxAllowedHeight

// To avoid jumping up and down while typing, use the lower screen when
// typing on upper, and vice versa
if position.midY / screenRect.height >= 0.5 {
panelRect.origin.y = position.minY - SquirrelTheme.offsetHeight - panelRect.height + theme.pagingOffset
if requiresFullScreen {
// --- 動態長寬比優化 ---
// 解決全屏縮放時,等比縮小導致「行長物理縮減、窗口變窄」的空間浪費問題
let area = contentRect.width * contentRect.height
let screenRatio = maxAllowedWidth / maxAllowedHeight

let optimalTextWidth: CGFloat
if vertical {
// 直排:自然寬度=高,自然高度=寬。算出完美契合螢幕比例的虛擬行長
optimalTextWidth = sqrt(area / screenRatio)
} else {
panelRect.origin.y = position.maxY + SquirrelTheme.offsetHeight
// 橫排:算出完美契合螢幕比例的虛擬行長
optimalTextWidth = sqrt(area * screenRatio)
}
// Make the first candidate fixed at the left of cursor
panelRect.origin.x = position.minX - panelRect.width - SquirrelTheme.offsetHeight
if view.preeditRange.length > 0, let preeditTextRange = view.convert(range: view.preeditRange) {
let preeditRect = view.contentRect(range: preeditTextRange)
panelRect.origin.x += preeditRect.height + theme.edgeInset.width

// 如果最佳行長大於原本設定的限制,就放開限制進行第二次完美排版
if optimalTextWidth > textWidth {
textWidth = optimalTextWidth
view.textContainer.size = NSSize(width: textWidth, height: .greatestFiniteMagnitude)
view.textLayoutManager.ensureLayout(for: view.textLayoutManager.documentRange)

contentRect = view.contentRect

if vertical {
naturalPanelSize.width = contentRect.height + theme.edgeInset.height * 2
naturalPanelSize.height = contentRect.width + theme.edgeInset.width * 2 + theme.pagingOffset
} else {
naturalPanelSize.width = contentRect.width + theme.edgeInset.width * 2 + theme.pagingOffset
naturalPanelSize.height = contentRect.height + theme.edgeInset.height * 2
}
}
} else {
panelRect.size = NSSize(width: min(0.95 * screenRect.width, contentRect.width + theme.edgeInset.width * 2),
height: min(0.95 * screenRect.height, contentRect.height + theme.edgeInset.height * 2))
panelRect.size.width += theme.pagingOffset
panelRect.origin = NSPoint(x: position.minX - theme.pagingOffset, y: position.minY - SquirrelTheme.offsetHeight - panelRect.height)
}
if panelRect.maxX > screenRect.maxX {
panelRect.origin.x = screenRect.maxX - panelRect.width
}
if panelRect.minX < screenRect.minX {
panelRect.origin.x = screenRect.minX
}
if panelRect.minY < screenRect.minY {

var panelRect = NSRect.zero

if requiresFullScreen {
// --- 全屏縮放模式 ---
let scaleX = maxAllowedWidth / naturalPanelSize.width
let scaleY = maxAllowedHeight / naturalPanelSize.height
let scale = min(scaleX, scaleY) // 保持等比縮小

// 窗口實際物理大小被縮小
panelRect.size = NSSize(width: naturalPanelSize.width * scale, height: naturalPanelSize.height * scale)

// 屏幕正中央對齊
panelRect.origin = NSPoint(
x: screenRect.minX + (screenRect.width - panelRect.width) / 2,
y: screenRect.minY + (screenRect.height - panelRect.height) / 2
)

maxHeight = 0 // 重置記憶尺寸緩存
} else {
// --- 常規跟隨光標模式 ---
// Apply memorizeSize
if theme.memorizeSize && (vertical && position.midY / screenRect.height < 0.5) ||
(vertical && position.minX + max(contentRect.width, maxHeight) + theme.edgeInset.width * 2 > screenRect.maxX) {
if contentRect.width >= maxHeight {
maxHeight = contentRect.width
} else {
contentRect.size.width = maxHeight
// 需要根據記憶寬度更新自然尺寸
if vertical {
naturalPanelSize.height = contentRect.width + theme.edgeInset.width * 2 + theme.pagingOffset
} else {
naturalPanelSize.width = contentRect.width + theme.edgeInset.width * 2 + theme.pagingOffset
}
}
}

panelRect.size = naturalPanelSize

if vertical {
panelRect.origin.y = screenRect.minY
// To avoid jumping up and down while typing
if position.midY / screenRect.height >= 0.5 {
panelRect.origin.y = position.minY - SquirrelTheme.offsetHeight - panelRect.height + theme.pagingOffset
} else {
panelRect.origin.y = position.maxY + SquirrelTheme.offsetHeight
}
panelRect.origin.x = position.minX - panelRect.width - SquirrelTheme.offsetHeight
if view.preeditRange.length > 0, let preeditTextRange = view.convert(range: view.preeditRange) {
let preeditRect = view.contentRect(range: preeditTextRange)
panelRect.origin.x += preeditRect.height + theme.edgeInset.width
}
} else {
panelRect.origin.y = position.maxY + SquirrelTheme.offsetHeight
panelRect.origin = NSPoint(x: position.minX - theme.pagingOffset, y: position.minY - SquirrelTheme.offsetHeight - panelRect.height)
}

// 常規模式下的邊界限制
if panelRect.maxX > screenRect.maxX { panelRect.origin.x = screenRect.maxX - panelRect.width }
if panelRect.minX < screenRect.minX { panelRect.origin.x = screenRect.minX }
if panelRect.minY < screenRect.minY {
if vertical { panelRect.origin.y = screenRect.minY } else { panelRect.origin.y = position.maxY + SquirrelTheme.offsetHeight }
}
if panelRect.maxY > screenRect.maxY { panelRect.origin.y = screenRect.maxY - panelRect.height }
if panelRect.minY < screenRect.minY { panelRect.origin.y = screenRect.minY }
}
if panelRect.maxY > screenRect.maxY {
panelRect.origin.y = screenRect.maxY - panelRect.height
}
if panelRect.minY < screenRect.minY {
panelRect.origin.y = screenRect.minY
}

self.setFrame(panelRect, display: true)

// contentView 的 frame 決定了它在窗口上的物理大小;
// contentView 的 bounds 決定了它內部的投影座標系。
// 將 bounds 設爲 naturalPanelSize(自然尺寸),視圖內部畫的一切東西就會自動被縮小到窗口(frame)裏
contentView!.frame = NSRect(origin: .zero, size: panelRect.size)
contentView!.bounds = NSRect(origin: .zero, size: naturalPanelSize)

// rotate the view, the core in vertical mode!
if vertical {
contentView!.boundsRotation = -90
contentView!.setBoundsOrigin(NSPoint(x: 0, y: panelRect.width))
contentView!.setBoundsOrigin(NSPoint(x: 0, y: naturalPanelSize.width))
} else {
contentView!.boundsRotation = 0
contentView!.setBoundsOrigin(.zero)
}

view.textView.boundsRotation = 0
view.textView.setBoundsOrigin(.zero)

view.frame = contentView!.bounds
view.textView.frame = contentView!.bounds
view.textView.frame.size.width -= theme.pagingOffset
view.textView.frame.origin.x += theme.pagingOffset
view.textView.textContainerInset = theme.edgeInset
// 下層組件必須讀取 contentView 旋轉後的真實 bounds
// (在 vertical 模式下,Cocoa 會自動將 bounds origin 偏移並交換長寬,確保內容在可見範圍內)
let subviewFrame = contentView!.bounds
view.frame = subviewFrame

var textFrame = subviewFrame
textFrame.size.width -= theme.pagingOffset
textFrame.origin.x += theme.pagingOffset
view.textView.frame = textFrame

if theme.translucency {
back.frame = contentView!.bounds
back.frame.size.width += theme.pagingOffset
var backFrame = subviewFrame
backFrame.size.width += theme.pagingOffset
back.frame = backFrame
back.appearance = NSApp.effectiveAppearance
back.isHidden = false
} else {
back.isHidden = true
}

alphaValue = theme.alpha
invalidateShadow()
orderFront(nil)
Expand Down
9 changes: 5 additions & 4 deletions sources/SquirrelView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ final class SquirrelView: NSView {
y1 = max(rect.maxY, y1)
}
}
return NSRect(x: x0, y: y0, width: x1-x0, height: y1-y0)
if x1 == -CGFloat.infinity { return .zero }
return NSRect(x: min(0, x0), y: min(0, y0), width: x1 - min(0, x0), height: y1 - min(0, y0))
}
// Get the rectangle containing the range of text, will first convert to glyph range, expensive to calculate
func contentRect(range: NSTextRange) -> NSRect {
Expand Down Expand Up @@ -137,7 +138,7 @@ final class SquirrelView: NSView {
var highlightedPreeditPath: CGMutablePath?
let theme = currentTheme

var containingRect = dirtyRect
var containingRect = self.bounds
containingRect.size.width -= theme.pagingOffset
let backgroundRect = containingRect

Expand Down Expand Up @@ -289,13 +290,13 @@ final class SquirrelView: NSView {
}
panelLayer.setAffineTransform(CGAffineTransform(translationX: theme.pagingOffset, y: 0))
let panelPath = CGMutablePath()
panelPath.addPath(backgroundPath!, transform: panelLayer.affineTransform().scaledBy(x: 1, y: -1).translatedBy(x: 0, y: -dirtyRect.height))
panelPath.addPath(backgroundPath!, transform: panelLayer.affineTransform().scaledBy(x: 1, y: -1).translatedBy(x: 0, y: -self.bounds.height))

let (pagingLayer, downPath, upPath) = pagingLayer(theme: theme, preeditRect: preeditRect)
if let sublayers = pagingLayer.sublayers, !sublayers.isEmpty {
self.layer?.addSublayer(pagingLayer)
}
let flipTransform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -dirtyRect.height)
let flipTransform = CGAffineTransform(scaleX: 1, y: -1).translatedBy(x: 0, y: -self.bounds.height)
if let downPath {
panelPath.addPath(downPath, transform: flipTransform)
self.downPath = downPath.copy()
Expand Down
Loading