vChewing-macOS/Packages/vChewing_CandidateWindow/Sources/CandidateWindow/CandidatePool_CocoaImpl.swift

312 lines
13 KiB
Swift
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2022 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import AppKit
// MARK: - UI Metrics.
extension CandidatePool {
public func updateMetrics() {
//
let initialOrigin: NSPoint = .init(x: originDelta, y: originDelta)
var totalAccuSize: NSSize = .zero
// Origin is at the top-left corner.
var currentOrigin: NSPoint = initialOrigin
var highlightedCellRect: CGRect = .zero
var highlightedLineRect: CGRect = .zero
var currentPageLines = candidateLines[lineRangeForCurrentPage]
var blankLines = maxLinesPerPage - currentPageLines.count
var fillBlankCells = true
switch (layout, isMatrix) {
case (.horizontal, false):
blankLines = 0
fillBlankCells = false
case (.vertical, false): blankLines = 0
case (_, true): break
}
while blankLines > 0 {
currentPageLines.append(.init(repeating: Self.shitCell, count: maxLineCapacity))
blankLines -= 1
}
Self.shitCell.updateMetrics(pool: self, origin: currentOrigin)
Self.shitCell.isHighlighted = false
let minimumCellDimension = Self.shitCell.visualDimension
currentPageLines.forEach { currentLine in
let currentLineOrigin = currentOrigin
var accumulatedLineSize: NSSize = .zero
var currentLineRect: CGRect { .init(origin: currentLineOrigin, size: accumulatedLineSize) }
let lineHasHighlightedCell = currentLine.hasHighlightedCell
currentLine.forEach { currentCell in
currentCell.updateMetrics(pool: self, origin: currentOrigin)
var cellDimension = currentCell.visualDimension
if layout == .vertical || currentCell.displayedText.count <= 2 {
cellDimension.width = max(minimumCellDimension.width, cellDimension.width)
}
cellDimension.height = max(minimumCellDimension.height, cellDimension.height)
switch self.layout {
case .horizontal:
accumulatedLineSize.width += cellDimension.width
accumulatedLineSize.height = max(accumulatedLineSize.height, cellDimension.height)
case .vertical:
accumulatedLineSize.height += cellDimension.height
accumulatedLineSize.width = max(accumulatedLineSize.width, cellDimension.width)
}
if lineHasHighlightedCell {
switch self.layout {
case .horizontal where currentCell.isHighlighted: highlightedCellRect.size.width = cellDimension.width
case .vertical: highlightedCellRect.size.width = max(highlightedCellRect.size.width, cellDimension.width)
default: break
}
if currentCell.isHighlighted {
highlightedCellRect.origin = currentOrigin
highlightedCellRect.size.height = cellDimension.height
}
}
switch self.layout {
case .horizontal: currentOrigin.x += cellDimension.width
case .vertical: currentOrigin.y += cellDimension.height
}
}
if lineHasHighlightedCell {
highlightedLineRect.origin = currentLineRect.origin
switch self.layout {
case .horizontal:
highlightedLineRect.size.height = currentLineRect.size.height
case .vertical:
highlightedLineRect.size.width = currentLineRect.size.width
}
}
switch self.layout {
case .horizontal:
highlightedLineRect.size.width = max(currentLineRect.size.width, highlightedLineRect.width)
case .vertical:
highlightedLineRect.size.height = max(currentLineRect.size.height, highlightedLineRect.height)
currentLine.forEach { theCell in
theCell.visualDimension.width = accumulatedLineSize.width
}
}
//
switch self.layout {
case .horizontal:
currentOrigin.x = originDelta
currentOrigin.y += accumulatedLineSize.height
totalAccuSize.width = max(totalAccuSize.width, accumulatedLineSize.width)
totalAccuSize.height += accumulatedLineSize.height
case .vertical:
currentOrigin.y = originDelta
currentOrigin.x += accumulatedLineSize.width
totalAccuSize.height = max(totalAccuSize.height, accumulatedLineSize.height)
totalAccuSize.width += accumulatedLineSize.width
}
}
if fillBlankCells {
switch layout {
case .horizontal:
totalAccuSize.width = max(totalAccuSize.width, CGFloat(maxLineCapacity) * minimumCellDimension.width)
highlightedLineRect.size.width = totalAccuSize.width
case .vertical:
totalAccuSize.height = CGFloat(maxLineCapacity) * minimumCellDimension.height
}
}
//
let strPeripherals = attributedDescriptionBottomPanes
var dimensionPeripherals = strPeripherals.boundingDimension
dimensionPeripherals.width = ceil(dimensionPeripherals.width)
dimensionPeripherals.height = ceil(dimensionPeripherals.height)
if finalContainerOrientation == .horizontal {
totalAccuSize.width += 5
dimensionPeripherals.width += 5
let delta = max(CandidateCellData.unifiedTextHeight + padding * 2 - dimensionPeripherals.height, 0)
currentOrigin = .init(x: totalAccuSize.width + originDelta, y: ceil(delta / 2) + originDelta)
totalAccuSize.width += dimensionPeripherals.width
} else {
totalAccuSize.height += 2
currentOrigin = .init(x: padding + originDelta, y: totalAccuSize.height + originDelta)
totalAccuSize.height += dimensionPeripherals.height
totalAccuSize.width = max(totalAccuSize.width, dimensionPeripherals.width)
}
let rectPeripherals = CGRect(origin: currentOrigin, size: dimensionPeripherals)
totalAccuSize.width += originDelta * 2
totalAccuSize.height += originDelta * 2
metrics = .init(fittingSize: totalAccuSize, highlightedLine: highlightedLineRect, highlightedCandidate: highlightedCellRect, peripherals: rectPeripherals)
}
private var finalContainerOrientation: NSUserInterfaceLayoutOrientation {
if maxLinesPerPage == 1, layout == .horizontal { return .horizontal }
return .vertical
}
}
// MARK: - Using One Single NSAttributedString. (Some of them are for debug purposes.)
extension CandidatePool {
// MARK: Candidate List with Peripherals.
public var attributedDescription: NSAttributedString {
switch layout {
case .horizontal: return attributedDescriptionHorizontal
case .vertical: return attributedDescriptionVertical
}
}
private var sharedParagraphStyle: NSParagraphStyle { CandidateCellData.sharedParagraphStyle }
private var attributedDescriptionHorizontal: NSAttributedString {
let paragraphStyle = sharedParagraphStyle
let attrCandidate: [NSAttributedString.Key: Any] = [
.kern: 0,
.font: Self.blankCell.phraseFont(size: Self.blankCell.size),
.paragraphStyle: paragraphStyle,
]
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
let spacer = NSAttributedString(string: " ", attributes: attrCandidate)
let lineFeed = NSAttributedString(string: "\n", attributes: attrCandidate)
for lineID in lineRangeForCurrentPage {
let arrLine = candidateLines[lineID]
arrLine.enumerated().forEach { cellID, currentCell in
let cellString = NSMutableAttributedString(
attributedString: currentCell.attributedString(
noSpacePadding: false, withHighlight: true, isMatrix: isMatrix
)
)
if lineID != currentLineNumber {
cellString.addAttribute(
.foregroundColor, value: NSColor.gray,
range: .init(location: 0, length: cellString.string.utf16.count)
)
}
result.append(cellString)
if cellID < arrLine.count - 1 {
result.append(spacer)
}
}
if lineID < lineRangeForCurrentPage.upperBound - 1 || isMatrix {
result.append(lineFeed)
} else {
result.append(spacer)
}
}
//
result.append(attributedDescriptionBottomPanes)
return result
}
private var attributedDescriptionVertical: NSAttributedString {
let paragraphStyle = sharedParagraphStyle
let attrCandidate: [NSAttributedString.Key: Any] = [
.kern: 0,
.font: Self.blankCell.phraseFont(size: Self.blankCell.size),
.paragraphStyle: paragraphStyle,
]
let result = NSMutableAttributedString(string: "", attributes: attrCandidate)
let spacer = NSMutableAttributedString(string: " ", attributes: attrCandidate)
let lineFeed = NSAttributedString(string: "\n", attributes: attrCandidate)
for (inlineIndex, _) in selectionKeys.enumerated() {
for (lineID, lineData) in candidateLines.enumerated() {
if !fallbackedLineRangeForCurrentPage.contains(lineID) { continue }
if !(0 ..< lineData.count).contains(inlineIndex) { continue }
let currentCell = lineData[inlineIndex]
let cellString = NSMutableAttributedString(
attributedString: currentCell.attributedString(
noSpacePadding: false, withHighlight: true, isMatrix: isMatrix
)
)
if lineID != currentLineNumber {
cellString.addAttribute(
.foregroundColor, value: NSColor.gray,
range: .init(location: 0, length: cellString.string.utf16.count)
)
}
result.append(cellString)
if isMatrix, currentCell.displayedText.count > 1 {
if currentCell.isHighlighted {
spacer.addAttribute(
.backgroundColor,
value: currentCell.themeColorCocoa,
range: .init(location: 0, length: spacer.string.utf16.count)
)
} else {
spacer.removeAttribute(
.backgroundColor,
range: .init(location: 0, length: spacer.string.utf16.count)
)
}
result.append(spacer)
}
}
result.append(lineFeed)
}
//
result.append(attributedDescriptionBottomPanes)
return result
}
// MARK: Peripherals
public var attributedDescriptionBottomPanes: NSAttributedString {
let paragraphStyle = sharedParagraphStyle
let result = NSMutableAttributedString(string: "")
result.append(attributedDescriptionPositionCounter)
if !tooltip.isEmpty { result.append(attributedDescriptionTooltip) }
if !reverseLookupResult.isEmpty { result.append(attributedDescriptionReverseLookp) }
result.addAttribute(.paragraphStyle, value: paragraphStyle, range: .init(location: 0, length: result.string.utf16.count))
return result
}
private var attributedDescriptionPositionCounter: NSAttributedString {
let positionCounterColorBG = NSApplication.isDarkMode
? NSColor(white: 0.215, alpha: 0.7)
: NSColor(white: 0.9, alpha: 0.7)
let positionCounterColorText = NSColor.controlTextColor
let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11)
let attrPositionCounter: [NSAttributedString.Key: Any] = [
.kern: 0,
.font: Self.blankCell.phraseFontEmphasized(size: positionCounterTextSize),
.backgroundColor: positionCounterColorBG,
.foregroundColor: positionCounterColorText,
]
let positionCounter = NSAttributedString(
string: " \(currentPositionLabelText) ", attributes: attrPositionCounter
)
return positionCounter
}
private var attributedDescriptionTooltip: NSAttributedString {
let positionCounterTextSize = max(ceil(CandidateCellData.unifiedSize * 0.7), 11)
let attrTooltip: [NSAttributedString.Key: Any] = [
.kern: 0,
.font: Self.blankCell.phraseFontEmphasized(size: positionCounterTextSize),
.foregroundColor: NSColor.textColor,
]
let tooltipText = NSAttributedString(
string: " \(tooltip) ", attributes: attrTooltip
)
return tooltipText
}
private var attributedDescriptionReverseLookp: NSAttributedString {
let reverseLookupTextSize = max(ceil(CandidateCellData.unifiedSize * 0.6), 9)
let attrReverseLookup: [NSAttributedString.Key: Any] = [
.kern: 0,
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
.foregroundColor: NSColor.textColor,
]
let attrReverseLookupSpacer: [NSAttributedString.Key: Any] = [
.kern: 0,
.font: Self.blankCell.phraseFont(size: reverseLookupTextSize),
]
let result = NSMutableAttributedString(string: "", attributes: attrReverseLookupSpacer)
for neta in reverseLookupResult {
result.append(NSAttributedString(string: " ", attributes: attrReverseLookupSpacer))
result.append(NSAttributedString(string: " \(neta) ", attributes: attrReverseLookup))
if maxLinesPerPage == 1 { break }
}
return result
}
}