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

391 lines
16 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.

This file contains Unicode characters that might be confused with other characters. 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 Foundation
import Shared
///
public struct CandidatePool {
public let blankCell: CandidateCellData
public let shitCell: CandidateCellData // cell
public let maxLinesPerPage: Int
public let layout: LayoutOrientation
public let selectionKeys: String
public let candidateDataAll: [CandidateCellData]
public var candidateLines: [[CandidateCellData]] = []
public var tooltip: String = ""
public var reverseLookupResult: [String] = []
public private(set) var highlightedIndex: Int = 0
public private(set) var currentLineNumber = 0
private var recordedLineRangeForCurrentPage: Range<Int>?
private var previouslyRecordedLineRangeForPreviousPage: Range<Int>?
// MARK: -
///
///
public var maxRowWidth: Double { ceil(Double(maxLineCapacity) * blankCell.cellLength()) }
///
public var currentPositionLabelText: String {
(highlightedIndex + 1).description + "/" + candidateDataAll.count.description
}
///
public var currentCandidate: CandidateCellData? {
(0 ..< candidateDataAll.count).contains(highlightedIndex) ? candidateDataAll[highlightedIndex] : nil
}
///
public var currentSelectedCandidateText: String? { currentCandidate?.displayedText ?? nil }
/// /
public var maxLineCapacity: Int { selectionKeys.count }
///
public var dummyCellsRequiredForCurrentLine: Int {
maxLineCapacity - candidateLines[currentLineNumber].count
}
///
public var lineRangeForFinalPageBlanked: Range<Int> {
0 ..< (maxLinesPerPage - lineRangeForCurrentPage.count)
}
///
public var lineRangeForCurrentPage: Range<Int> {
recordedLineRangeForCurrentPage ?? fallbackedLineRangeForCurrentPage
}
/// 退
public var fallbackedLineRangeForCurrentPage: Range<Int> {
currentLineNumber ..< min(candidateLines.count, currentLineNumber + maxLinesPerPage)
}
// MARK: - Constructors
///
/// - Parameters:
/// - candidates:
/// - selectionKeys:
/// - direction:
/// - locale: zh-Hanszh-Hant
public init(
candidates: [String], lines: Int = 3, selectionKeys: String = "123456789",
layout: LayoutOrientation = .vertical, locale: String = ""
) {
self.layout = layout
maxLinesPerPage = max(1, lines)
blankCell = CandidateCellData(key: " ", displayedText: " ", isSelected: false)
shitCell = CandidateCellData(key: " ", displayedText: "💩", isSelected: false)
blankCell.locale = locale
self.selectionKeys = selectionKeys.isEmpty ? "123456789" : selectionKeys
var allCandidates = candidates.map { CandidateCellData(key: " ", displayedText: $0) }
if allCandidates.isEmpty { allCandidates.append(blankCell) }
candidateDataAll = allCandidates
var currentColumn: [CandidateCellData] = []
for (i, candidate) in candidateDataAll.enumerated() {
candidate.index = i
candidate.whichLine = candidateLines.count
var isOverflown: Bool = (currentColumn.count == maxLineCapacity) && !currentColumn.isEmpty
if layout == .horizontal {
isOverflown = isOverflown
|| currentColumn.map { $0.cellLength() }.reduce(0, +) > maxRowWidth - candidate.cellLength()
}
if isOverflown {
candidateLines.append(currentColumn)
currentColumn.removeAll()
candidate.whichLine += 1
}
candidate.subIndex = currentColumn.count
candidate.locale = locale
currentColumn.append(candidate)
}
candidateLines.append(currentColumn)
recordedLineRangeForCurrentPage = fallbackedLineRangeForCurrentPage
highlight(at: 0)
}
}
// MARK: - Public Functions (for all OS)
public extension CandidatePool {
///
enum LayoutOrientation {
case horizontal
case vertical
}
///
/// - Parameter isBackward:
/// - Returns:
@discardableResult mutating func flipPage(isBackward: Bool) -> Bool {
backupLineRangeForCurrentPage()
defer { flipLineRangeToNeighborPage(isBackward: isBackward) }
return consecutivelyFlipLines(isBackward: isBackward, count: maxLinesPerPage)
}
///
/// - Parameter subIndex:
/// - Returns: nil
func calculateCandidateIndex(subIndex: Int) -> Int? {
let arrCurrentLine = candidateLines[currentLineNumber]
if !(0 ..< arrCurrentLine.count).contains(subIndex) { return nil }
return arrCurrentLine[subIndex].index
}
///
/// - Parameters:
/// - isBackward:
/// - count:
/// - Returns:
@discardableResult mutating func consecutivelyFlipLines(isBackward: Bool, count: Int) -> Bool {
switch isBackward {
case false where currentLineNumber == candidateLines.count - 1:
return highlightNeighborCandidate(isBackward: false)
case true where currentLineNumber == 0:
return highlightNeighborCandidate(isBackward: true)
default:
if count <= 0 { return false }
for _ in 0 ..< min(maxLinesPerPage, count) {
selectNewNeighborLine(isBackward: isBackward)
}
return true
}
}
///
/// - Parameter isBackward:
/// - Returns:
@discardableResult mutating func highlightNeighborCandidate(isBackward: Bool) -> Bool {
switch isBackward {
case false where highlightedIndex >= candidateDataAll.count - 1:
highlight(at: 0)
return false
case true where highlightedIndex <= 0:
highlight(at: candidateDataAll.count - 1)
return false
default:
highlight(at: highlightedIndex + (isBackward ? -1 : 1))
return true
}
}
///
/// - Parameter indexSpecified:
mutating func highlight(at indexSpecified: Int) {
var indexSpecified = indexSpecified
let isBackward: Bool = indexSpecified > highlightedIndex
highlightedIndex = indexSpecified
if !(0 ..< candidateDataAll.count).contains(highlightedIndex) {
switch highlightedIndex {
case candidateDataAll.count...:
currentLineNumber = candidateLines.count - 1
highlightedIndex = max(0, candidateDataAll.count - 1)
indexSpecified = highlightedIndex
case ..<0:
highlightedIndex = 0
currentLineNumber = 0
indexSpecified = highlightedIndex
default: break
}
}
for (i, candidate) in candidateDataAll.enumerated() {
candidate.isHighlighted = (indexSpecified == i)
if candidate.isHighlighted { currentLineNumber = candidate.whichLine }
}
for (i, candidateColumn) in candidateLines.enumerated() {
if i != currentLineNumber {
candidateColumn.forEach {
$0.key = " "
}
} else {
for (i, neta) in candidateColumn.enumerated() {
if neta.key.isEmpty { continue }
neta.key = selectionKeys.map(\.description)[i]
}
}
}
if highlightedIndex != 0, indexSpecified == 0 {
recordedLineRangeForCurrentPage = fallbackedLineRangeForCurrentPage
} else {
fixLineRange(isBackward: isBackward)
}
}
func cellWidth(_ cell: CandidateCellData) -> (min: CGFloat?, max: CGFloat?) {
let minAccepted = ceil(shitCell.cellLength(isMatrix: false))
let defaultMin: CGFloat = cell.cellLength(isMatrix: maxLinesPerPage != 1)
var min: CGFloat = defaultMin
if layout != .vertical, maxLinesPerPage == 1 {
min = max(minAccepted, cell.cellLength(isMatrix: false))
} else if layout == .vertical, maxLinesPerPage == 1 {
min = max(Double(CandidateCellData.unifiedSize * 6), 90)
}
return (min, nil)
}
}
// MARK: - Privates.
private extension CandidatePool {
enum VerticalDirection {
case up
case down
}
enum HorizontalDirection {
case left
case right
}
///
var lineRangeForFirstPage: Range<Int> {
0 ..< min(maxLinesPerPage, candidateLines.count)
}
///
var lineRangeForFinalPage: Range<Int> {
max(0, candidateLines.count - maxLinesPerPage) ..< candidateLines.count
}
mutating func selectNewNeighborLine(isBackward: Bool) {
switch layout {
case .horizontal: selectNewNeighborRow(direction: isBackward ? .up : .down)
case .vertical: selectNewNeighborColumn(direction: isBackward ? .left : .right)
}
}
mutating func fixLineRange(isBackward: Bool = false) {
if !lineRangeForCurrentPage.contains(currentLineNumber) {
switch isBackward {
case false:
let theMin = currentLineNumber
let theMax = min(theMin + maxLinesPerPage, candidateLines.count)
recordedLineRangeForCurrentPage = theMin ..< theMax
case true:
let theMax = currentLineNumber + 1
let theMin = max(0, theMax - maxLinesPerPage)
recordedLineRangeForCurrentPage = theMin ..< theMax
}
}
}
mutating func backupLineRangeForCurrentPage() {
previouslyRecordedLineRangeForPreviousPage = lineRangeForCurrentPage
}
mutating func flipLineRangeToNeighborPage(isBackward: Bool = false) {
guard let prevRange = previouslyRecordedLineRangeForPreviousPage else { return }
var lowerBound = prevRange.lowerBound
var upperBound = prevRange.upperBound
//
lowerBound += maxLinesPerPage * (isBackward ? -1 : 1)
upperBound += maxLinesPerPage * (isBackward ? -1 : 1)
//
branch1: switch isBackward {
case false:
if upperBound < candidateLines.count { break branch1 }
if lowerBound < lineRangeForFinalPage.lowerBound { break branch1 }
let isOverFlipped = !lineRangeForFinalPage.contains(currentLineNumber)
recordedLineRangeForCurrentPage = isOverFlipped ? lineRangeForFirstPage : lineRangeForFinalPage
return
case true:
if lowerBound > 0 { break branch1 }
if upperBound > lineRangeForFirstPage.upperBound { break branch1 }
let isOverFlipped = !lineRangeForFirstPage.contains(currentLineNumber)
recordedLineRangeForCurrentPage = isOverFlipped ? lineRangeForFinalPage : lineRangeForFirstPage
return
}
let result = lowerBound ..< upperBound
if result.contains(currentLineNumber) {
recordedLineRangeForCurrentPage = result
return
}
//
}
mutating func selectNewNeighborRow(direction: VerticalDirection) {
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
var result = currentSubIndex
branch: switch direction {
case .up:
if currentLineNumber <= 0 {
if candidateLines.isEmpty { break }
let firstRow = candidateLines[0]
let newSubIndex = min(currentSubIndex, firstRow.count - 1)
highlight(at: firstRow[newSubIndex].index)
fixLineRange(isBackward: false)
break branch
}
if currentLineNumber >= candidateLines.count - 1 { currentLineNumber = candidateLines.count - 1 }
result = currentSubIndex
//
// if candidateLines[currentLineNumber].count != candidateLines[currentLineNumber - 1].count {
// let ratio: Double = min(1, Double(currentSubIndex) / Double(candidateLines[currentLineNumber].count))
// result = max(Int(floor(Double(candidateLines[currentLineNumber - 1].count) * ratio)), result)
// }
let targetRow = candidateLines[currentLineNumber - 1]
let newSubIndex = min(result, targetRow.count - 1)
highlight(at: targetRow[newSubIndex].index)
fixLineRange(isBackward: true)
case .down:
if currentLineNumber >= candidateLines.count - 1 {
if candidateLines.isEmpty { break }
let finalRow = candidateLines[candidateLines.count - 1]
let newSubIndex = min(currentSubIndex, finalRow.count - 1)
highlight(at: finalRow[newSubIndex].index)
fixLineRange(isBackward: true)
break branch
}
result = currentSubIndex
//
if candidateLines[currentLineNumber].count != candidateLines[currentLineNumber + 1].count {
let ratio: Double = min(1, Double(currentSubIndex) / Double(candidateLines[currentLineNumber].count))
result = max(Int(floor(Double(candidateLines[currentLineNumber + 1].count) * ratio)), result)
}
let targetRow = candidateLines[currentLineNumber + 1]
let newSubIndex = min(result, targetRow.count - 1)
highlight(at: targetRow[newSubIndex].index)
fixLineRange(isBackward: false)
}
}
mutating func selectNewNeighborColumn(direction: HorizontalDirection) {
let currentSubIndex = candidateDataAll[highlightedIndex].subIndex
switch direction {
case .left:
if currentLineNumber <= 0 {
if candidateLines.isEmpty { break }
let firstColumn = candidateLines[0]
let newSubIndex = min(currentSubIndex, firstColumn.count - 1)
highlight(at: firstColumn[newSubIndex].index)
break
}
if currentLineNumber >= candidateLines.count - 1 { currentLineNumber = candidateLines.count - 1 }
let targetColumn = candidateLines[currentLineNumber - 1]
let newSubIndex = min(currentSubIndex, targetColumn.count - 1)
highlight(at: targetColumn[newSubIndex].index)
fixLineRange(isBackward: true)
case .right:
if currentLineNumber >= candidateLines.count - 1 {
if candidateLines.isEmpty { break }
let finalColumn = candidateLines[candidateLines.count - 1]
let newSubIndex = min(currentSubIndex, finalColumn.count - 1)
highlight(at: finalColumn[newSubIndex].index)
break
}
let targetColumn = candidateLines[currentLineNumber + 1]
let newSubIndex = min(currentSubIndex, targetColumn.count - 1)
highlight(at: targetColumn[newSubIndex].index)
fixLineRange(isBackward: false)
}
}
}