From 1cc2b31b732b5d49d715508a66e58210d7b2159a Mon Sep 17 00:00:00 2001 From: LEO Yoon-Tsaw Date: Sun, 23 Jun 2024 13:36:38 -0400 Subject: [PATCH] add paging indicators --- sources/SquirrelInputController.swift | 9 +- sources/SquirrelPanel.swift | 50 +++++++--- sources/SquirrelTheme.swift | 6 ++ sources/SquirrelView.swift | 135 ++++++++++++++++++++++---- 4 files changed, 166 insertions(+), 34 deletions(-) diff --git a/sources/SquirrelInputController.swift b/sources/SquirrelInputController.swift index 3b4861a11..5e2956e00 100644 --- a/sources/SquirrelInputController.swift +++ b/sources/SquirrelInputController.swift @@ -501,10 +501,13 @@ private extension SquirrelInputController { } } // swiftlint:enable identifier_name + let page = Int(ctx.menu.page_no) + let lastPage = ctx.menu.is_last_page let selRange = NSRange(location: start.utf16Offset(in: preedit), length: preedit.utf16.distance(from: start, to: end)) showPanel(preedit: inlinePreedit ? "" : preedit, selRange: selRange, caretPos: caretPos.utf16Offset(in: preedit), - candidates: candidates, comments: comments, labels: labels, highlighted: Int(ctx.menu.highlighted_candidate_index)) + candidates: candidates, comments: comments, labels: labels, highlighted: Int(ctx.menu.highlighted_candidate_index), + page: page, lastPage: lastPage) _ = rimeAPI.free_context(&ctx) } else { hidePalettes() @@ -544,7 +547,7 @@ private extension SquirrelInputController { } // swiftlint:disable:next function_parameter_count - func showPanel(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted: Int) { + func showPanel(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted: Int, page: Int, lastPage: Bool) { // print("[DEBUG] showPanelWithPreedit:...:") guard let client = client else { return } var inputPos = NSRect() @@ -552,7 +555,7 @@ private extension SquirrelInputController { if let panel = NSApp.squirrelAppDelegate.panel { panel.position = inputPos panel.inputController = self - panel.update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: highlighted, update: true) + panel.update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: highlighted, page: page, lastPage: lastPage, update: true) } } } diff --git a/sources/SquirrelPanel.swift b/sources/SquirrelPanel.swift index f0e1e3f3f..5fef16c76 100644 --- a/sources/SquirrelPanel.swift +++ b/sources/SquirrelPanel.swift @@ -29,6 +29,9 @@ final class SquirrelPanel: NSPanel { private var cursorIndex: Int = 0 private var scrollDirection: CGVector = .zero private var scrollTime: Date = .distantPast + private var page: Int = 0 + private var lastPage: Bool = true + private var pagingUp: Bool? init(position: NSRect) { self.position = position @@ -68,20 +71,31 @@ final class SquirrelPanel: NSPanel { override func sendEvent(_ event: NSEvent) { switch event.type { case .leftMouseDown: - let (index, _) = view.click(at: mousePosition()) - if let index = index, index >= 0 && index < candidates.count { + let (index, _, pagingUp) = view.click(at: mousePosition()) + if let pagingUp { + self.pagingUp = pagingUp + } else { + self.pagingUp = nil + } + if let index, index >= 0 && index < candidates.count { self.index = index } case .leftMouseUp: - let (index, preeditIndex) = view.click(at: mousePosition()) - if let preeditIndex = preeditIndex, preeditIndex >= 0 && preeditIndex < preedit.utf16.count { + let (index, preeditIndex, pagingUp) = view.click(at: mousePosition()) + + if let pagingUp, pagingUp == self.pagingUp { + _ = inputController?.page(up: pagingUp) + } else { + self.pagingUp = nil + } + if let preeditIndex, preeditIndex >= 0 && preeditIndex < preedit.utf16.count { if preeditIndex < caretPos { _ = inputController?.moveCaret(forward: true) } else if preeditIndex > caretPos { _ = inputController?.moveCaret(forward: false) } } - if let index = index, index == self.index && index >= 0 && index < candidates.count { + if let index, index == self.index && index >= 0 && index < candidates.count { _ = inputController?.selectCandidate(index) } case .mouseEntered: @@ -89,12 +103,13 @@ final class SquirrelPanel: NSPanel { case .mouseExited: acceptsMouseMovedEvents = false if cursorIndex != index { - update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, update: false) + update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, page: page, lastPage: lastPage, update: false) } + pagingUp = nil case .mouseMoved: - let (index, _) = view.click(at: mousePosition()) + let (index, _, _) = view.click(at: mousePosition()) if let index = index, cursorIndex != index && index >= 0 && index < candidates.count { - update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, update: false) + update(preedit: preedit, selRange: selRange, caretPos: caretPos, candidates: candidates, comments: comments, labels: labels, highlighted: index, page: page, lastPage: lastPage, update: false) } case .scrollWheel: if event.phase == .began { @@ -141,7 +156,7 @@ final class SquirrelPanel: NSPanel { // Main function to add attributes to text output from librime // swiftlint:disable:next cyclomatic_complexity function_parameter_count - func update(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted index: Int, update: Bool) { + func update(preedit: String, selRange: NSRange, caretPos: Int, candidates: [String], comments: [String], labels: [String], highlighted index: Int, page: Int, lastPage: Bool, update: Bool) { if update { self.preedit = preedit self.selRange = selRange @@ -150,6 +165,8 @@ final class SquirrelPanel: NSPanel { self.comments = comments self.labels = labels self.index = index + self.page = page + self.lastPage = lastPage } cursorIndex = index @@ -266,7 +283,7 @@ final class SquirrelPanel: NSPanel { // text done! view.textView.textContentStorage?.attributedString = text view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal) - view.drawView(candidateRanges: candidateRanges, hilightedIndex: index, preeditRange: preeditRange, highlightedPreeditRange: highlightedPreeditRange) + view.drawView(candidateRanges: candidateRanges, hilightedIndex: index, preeditRange: preeditRange, highlightedPreeditRange: highlightedPreeditRange, canPageUp: page > 0, canPageDown: !lastPage) show() } @@ -359,11 +376,12 @@ private extension SquirrelPanel { 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)) + height: min(0.95 * screenRect.height, contentRect.width + theme.edgeInset.width * 2) + theme.pagingOffset) + // 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 + panelRect.origin.y = position.minY - SquirrelTheme.offsetHeight - panelRect.height + theme.pagingOffset } else { panelRect.origin.y = position.maxY + SquirrelTheme.offsetHeight } @@ -376,7 +394,8 @@ private extension SquirrelPanel { } 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.origin = NSPoint(x: position.minX, y: position.minY - SquirrelTheme.offsetHeight - panelRect.height) + 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 @@ -412,10 +431,13 @@ private extension SquirrelPanel { 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 if theme.translucency { back.frame = contentView!.bounds + back.frame.size.width += theme.pagingOffset back.appearance = NSApp.effectiveAppearance back.isHidden = false } else { @@ -434,7 +456,7 @@ private extension SquirrelPanel { view.textContentStorage.attributedString = text view.textView.setLayoutOrientation(vertical ? .vertical : .horizontal) view.drawView(candidateRanges: [NSRange(location: 0, length: text.length)], hilightedIndex: -1, - preeditRange: .empty, highlightedPreeditRange: .empty) + preeditRange: .empty, highlightedPreeditRange: .empty, canPageUp: false, canPageDown: false) show() statusTimer?.invalidate() diff --git a/sources/SquirrelTheme.swift b/sources/SquirrelTheme.swift index 401e28ea3..25ff1c57b 100644 --- a/sources/SquirrelTheme.swift +++ b/sources/SquirrelTheme.swift @@ -65,6 +65,7 @@ final class SquirrelTheme { private(set) var vertical = false private(set) var inlinePreedit = false private(set) var inlineCandidate = false + private(set) var showPaging = false private var fonts = [NSFont]() private var labelFonts = [NSFont]() @@ -182,6 +183,9 @@ final class SquirrelTheme { _candidateFormat = newTemplate } } + var pagingOffset: CGFloat { + (labelFontSize ?? fontSize ?? Self.defaultFontSize) * 1.5 + } func load(config: SquirrelConfig, dark: Bool) { linear ?= config.getString("style/candidate_list_layout").map { $0 == "linear" } @@ -191,6 +195,7 @@ final class SquirrelTheme { translucency ?= config.getBool("style/translucency") mutualExclusive ?= config.getBool("style/mutual_exclusive") memorizeSize ?= config.getBool("style/memorize_size") + showPaging ?= config.getBool("style/show_paging") statusMessageType ?= .init(rawValue: config.getString("style/status_message_type") ?? "") candidateFormat ?= config.getString("style/candidate_format") @@ -244,6 +249,7 @@ final class SquirrelTheme { inlineCandidate ?= config.getBool("\(prefix)/inline_candidate") translucency ?= config.getBool("\(prefix)/translucency") mutualExclusive ?= config.getBool("\(prefix)/mutual_exclusive") + showPaging ?= config.getBool("\(prefix)/show_paging") candidateFormat ?= config.getString("\(prefix)/candidate_format") fontName ?= config.getString("\(prefix)/font_face") fontSize ?= config.getDouble("\(prefix)/font_point") diff --git a/sources/SquirrelView.swift b/sources/SquirrelView.swift index 4f590764f..23b0ab43e 100644 --- a/sources/SquirrelView.swift +++ b/sources/SquirrelView.swift @@ -29,9 +29,13 @@ final class SquirrelView: NSView { var candidateRanges: [NSRange] = [] var hilightedIndex = 0 var preeditRange: NSRange = .empty + var canPageUp: Bool = false + var canPageDown: Bool = false var highlightedPreeditRange: NSRange = .empty var separatorWidth: CGFloat = 0 var shape = CAShapeLayer() + private var downPath: CGPath? + private var upPath: CGPath? var lightTheme = SquirrelTheme() var darkTheme = SquirrelTheme() @@ -112,11 +116,13 @@ final class SquirrelView: NSView { } // Will triger - (void)drawRect:(NSRect)dirtyRect - func drawView(candidateRanges: [NSRange], hilightedIndex: Int, preeditRange: NSRange, highlightedPreeditRange: NSRange) { + func drawView(candidateRanges: [NSRange], hilightedIndex: Int, preeditRange: NSRange, highlightedPreeditRange: NSRange, canPageUp: Bool, canPageDown: Bool) { self.candidateRanges = candidateRanges self.hilightedIndex = hilightedIndex self.preeditRange = preeditRange self.highlightedPreeditRange = highlightedPreeditRange + self.canPageUp = canPageUp + self.canPageDown = canPageDown self.needsDisplay = true } @@ -130,8 +136,9 @@ final class SquirrelView: NSView { var highlightedPreeditPath: CGMutablePath? let theme = currentTheme - let backgroundRect = dirtyRect var containingRect = dirtyRect + containingRect.size.width -= theme.pagingOffset + let backgroundRect = containingRect // Draw preedit Rect var preeditRect = NSRect.zero @@ -210,7 +217,6 @@ final class SquirrelView: NSView { NSBezierPath.defaultLineWidth = 0 backgroundPath = drawSmoothLines(rectVertex(of: backgroundRect), straightCorner: Set(), alpha: 0.3 * theme.cornerRadius, beta: 1.4 * theme.cornerRadius) - shape.path = backgroundPath self.layer?.sublayers = nil let backPath = backgroundPath?.mutableCopy() @@ -280,14 +286,39 @@ final class SquirrelView: NSView { } panelLayer.addSublayer(layer) } + 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)) + + 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) + if let downPath { + panelPath.addPath(downPath, transform: flipTransform) + self.downPath = downPath.copy() + } + if let upPath { + panelPath.addPath(upPath, transform: flipTransform) + self.upPath = upPath.copy() + } + + shape.path = panelPath } - func click(at clickPoint: NSPoint) -> (Int?, Int?) { + func click(at clickPoint: NSPoint) -> (Int?, Int?, Bool?) { var index = 0 var candidateIndex: Int? var preeditIndex: Int? + if let downPath = self.downPath, downPath.contains(clickPoint) { + return (nil, nil, false) + } + if let upPath = self.upPath, upPath.contains(clickPoint) { + return (nil, nil, true) + } if let path = shape.path, path.contains(clickPoint) { - var point = NSPoint(x: clickPoint.x - textView.textContainerInset.width, + var point = NSPoint(x: clickPoint.x - textView.textContainerInset.width - currentTheme.pagingOffset, y: clickPoint.y - textView.textContainerInset.height) let fragment = textLayoutManager.textLayoutFragment(for: point) if let fragment = fragment { @@ -313,7 +344,7 @@ final class SquirrelView: NSView { } } } - return (candidateIndex, preeditIndex) + return (candidateIndex, preeditIndex, nil) } } @@ -329,9 +360,16 @@ private extension SquirrelView { } } + func scale(_ value: CGFloat, multiplier: CGFloat, divider: CGFloat) -> CGFloat { + if abs(value / divider) < 1e-3 { + return multiplier / divider + } + return sign(value / divider) * multiplier / value + } + // Bezier cubic curve, which has continuous roundness func drawSmoothLines(_ vertex: [NSPoint], straightCorner: Set, alpha: CGFloat, beta rawBeta: CGFloat) -> CGPath? { - guard vertex.count >= 4 else { + guard vertex.count >= 3 else { return nil } let beta = max(0.00001, rawBeta) @@ -343,9 +381,16 @@ private extension SquirrelView { var control2: NSPoint var target = previousPoint var diff = NSPoint(x: point.x - previousPoint.x, y: point.y - previousPoint.y) + var scaleFactor: CGFloat = 1 if straightCorner.isEmpty || !straightCorner.contains(vertex.count-1) { - target.x += sign(diff.x/beta)*beta - target.y += sign(diff.y/beta)*beta + if vertex.count > 3 { + target.x += sign(diff.x / beta) * beta + target.y += sign(diff.y / beta) * beta + } else { // triangle + scaleFactor = min(scale(diff.x, multiplier: beta, divider: beta), scale(diff.y, multiplier: beta, divider: beta)) + target.x += scaleFactor * diff.x + target.y += scaleFactor * diff.y + } } path.move(to: target) for i in 0.. 3 { + target.x -= sign(diff.x / beta) * beta + target.y -= sign(diff.y / beta) * beta + control1.x -= sign(diff.x / beta) * alpha + control1.y -= sign(diff.y / beta) * alpha + } else { // triangle + scaleFactor = min(scale(diff.x, multiplier: beta, divider: beta), scale(diff.y, multiplier: beta, divider: beta)) + target.x -= scaleFactor * diff.x + target.y -= scaleFactor * diff.y + scaleFactor = min(scale(diff.x, multiplier: alpha, divider: beta), scale(diff.y, multiplier: alpha, divider: beta)) + control1.x -= scaleFactor * diff.x + control1.y -= scaleFactor * diff.y + } path.addLine(to: target) target = point control2 = point diff = NSPoint(x: nextPoint.x - point.x, y: nextPoint.y - point.y) - control2.x += sign(diff.x/beta)*alpha - target.x += sign(diff.x/beta)*beta - control2.y += sign(diff.y/beta)*alpha - target.y += sign(diff.y/beta)*beta + if vertex.count > 3 { + target.x += sign(diff.x / beta) * beta + target.y += sign(diff.y / beta) * beta + control2.x += sign(diff.x / beta) * alpha + control2.y += sign(diff.y / beta) * alpha + } else { + scaleFactor = min(scale(diff.x, multiplier: beta, divider: beta), scale(diff.y, multiplier: beta, divider: beta)) + target.x += scaleFactor * diff.x + target.y += scaleFactor * diff.y + scaleFactor = min(scale(diff.x, multiplier: alpha, divider: beta), scale(diff.y, multiplier: beta, divider: beta)) + control2.x += scaleFactor * diff.x + control2.y += scaleFactor * diff.y + } path.addCurve(to: target, control1: control1, control2: control2) } @@ -699,4 +762,42 @@ private extension SquirrelView { newRect.origin.y += currentTheme.hilitedCornerRadius + currentTheme.borderWidth return newRect } + + func triangle(center: NSPoint, radius: CGFloat) -> [NSPoint] { + [NSPoint(x: center.x, y: center.y + radius), + NSPoint(x: center.x + 0.5 * sqrt(3) * radius, y: center.y - 0.5 * radius), + NSPoint(x: center.x - 0.5 * sqrt(3) * radius, y: center.y - 0.5 * radius)] + } + + func pagingLayer(theme: SquirrelTheme, preeditRect: CGRect) -> (CAShapeLayer, CGPath?, CGPath?) { + let layer = CAShapeLayer() + guard theme.showPaging && (canPageUp || canPageDown) else { return (layer, nil, nil) } + guard let firstCandidate = candidateRanges.first, let range = convert(range: firstCandidate) else { return (layer, nil, nil) } + var height = contentRect(range: range).height + let preeditHeight = max(0, preeditRect.height + theme.preeditLinespace / 2 + theme.hilitedCornerRadius / 2 - theme.edgeInset.height) + theme.edgeInset.height - theme.linespace / 2 + height += theme.linespace + let radius = min(0.5 * theme.pagingOffset, 2 * height / 9) + let effectiveRadius = min(theme.cornerRadius, 0.4 * radius) + guard let trianglePath = drawSmoothLines(triangle(center: NSPoint(x: 0, y: 0), radius: radius), + straightCorner: [], alpha: 0.3 * effectiveRadius, beta: 1.4 * effectiveRadius) else { + return (layer, nil, nil) + } + var downPath: CGPath? + var upPath: CGPath? + if canPageDown { + var downTransform = CGAffineTransform(translationX: 0.5 * theme.pagingOffset, y: 2 * height / 3 + preeditHeight) + let downLayer = shapeFromPath(path: trianglePath.copy(using: &downTransform)) + downLayer.fillColor = theme.backgroundColor.cgColor + downPath = trianglePath.copy(using: &downTransform) + layer.addSublayer(downLayer) + } + if canPageUp { + var upTransform = CGAffineTransform(rotationAngle: .pi).translatedBy(x: -0.5 * theme.pagingOffset, y: -height / 3 - preeditHeight) + let upLayer = shapeFromPath(path: trianglePath.copy(using: &upTransform)) + upLayer.fillColor = theme.backgroundColor.cgColor + upPath = trianglePath.copy(using: &upTransform) + layer.addSublayer(upLayer) + } + return (layer, downPath, upPath) + } }