forked from rime/squirrel
-
Notifications
You must be signed in to change notification settings - Fork 7
/
Copy pathSquirrelTheme.swift
340 lines (315 loc) · 15.2 KB
/
SquirrelTheme.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
//
// SquirrelTheme.swift
// Squirrel
//
// Created by Leo Liu on 5/9/24.
//
import AppKit
final class SquirrelTheme {
static let offsetHeight: CGFloat = 5
static let defaultFontSize: CGFloat = NSFont.systemFontSize
static let showStatusDuration: Double = 1.2
static let defaultFont = NSFont.userFont(ofSize: defaultFontSize)!
enum StatusMessageType: String {
case long, short, mix
}
enum RimeColorSpace {
case displayP3, sRGB
static func from(name: String) -> Self {
if name == "display_p3" {
return .displayP3
} else {
return .sRGB
}
}
}
private(set) var available = true
private(set) var native = true
private(set) var memorizeSize = true
private var colorSpace: RimeColorSpace = .sRGB
var backgroundColor: NSColor = .windowBackgroundColor
var highlightedPreeditColor: NSColor?
var highlightedBackColor: NSColor? = .selectedTextBackgroundColor
var preeditBackgroundColor: NSColor?
var candidateBackColor: NSColor?
var borderColor: NSColor?
private var textColor: NSColor = .tertiaryLabelColor
private var highlightedTextColor: NSColor = .labelColor
private var candidateTextColor: NSColor = .secondaryLabelColor
private var highlightedCandidateTextColor: NSColor = .labelColor
private var candidateLabelColor: NSColor?
private var highlightedCandidateLabelColor: NSColor?
private var commentTextColor: NSColor? = .tertiaryLabelColor
private var highlightedCommentTextColor: NSColor?
private(set) var cornerRadius: CGFloat = 0
private(set) var hilitedCornerRadius: CGFloat = 0
private(set) var surroundingExtraExpansion: CGFloat = 0
private(set) var shadowSize: CGFloat = 0
private(set) var borderWidth: CGFloat = 0
private(set) var borderHeight: CGFloat = 0
private(set) var linespace: CGFloat = 0
private(set) var preeditLinespace: CGFloat = 0
private(set) var baseOffset: CGFloat = 0
private(set) var alpha: CGFloat = 1
private(set) var translucency = false
private(set) var mutualExclusive = false
private(set) var linear = false
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]()
private var commentFonts = [NSFont]()
private var fontSize: CGFloat?
private var labelFontSize: CGFloat?
private var commentFontSize: CGFloat?
private var _candidateFormat = "[label]. [candidate] [comment]"
private(set) var statusMessageType: StatusMessageType = .mix
private var defaultFont: NSFont {
if let size = fontSize {
Self.defaultFont.withSize(size)
} else {
Self.defaultFont
}
}
private(set) lazy var font: NSFont = combineFonts(fonts, size: fontSize) ?? defaultFont
private(set) lazy var labelFont: NSFont = {
if let font = combineFonts(labelFonts, size: labelFontSize ?? fontSize) {
return font
} else if let size = labelFontSize {
return self.font.withSize(size)
} else {
return self.font
}
}()
private(set) lazy var commentFont: NSFont = {
if let font = combineFonts(commentFonts, size: commentFontSize ?? fontSize) {
return font
} else if let size = commentFontSize {
return self.font.withSize(size)
} else {
return self.font
}
}()
private(set) lazy var attrs: [NSAttributedString.Key: Any] = [
.foregroundColor: candidateTextColor,
.font: font,
.baselineOffset: baseOffset
]
private(set) lazy var highlightedAttrs: [NSAttributedString.Key: Any] = [
.foregroundColor: highlightedCandidateTextColor,
.font: font,
.baselineOffset: baseOffset
]
private(set) lazy var labelAttrs: [NSAttributedString.Key: Any] = [
.foregroundColor: candidateLabelColor ?? blendColor(foregroundColor: self.candidateTextColor, backgroundColor: self.backgroundColor),
.font: labelFont,
.baselineOffset: baseOffset + (!vertical ? (font.pointSize - labelFont.pointSize) / 2.5 : 0)
]
private(set) lazy var labelHighlightedAttrs: [NSAttributedString.Key: Any] = [
.foregroundColor: highlightedCandidateLabelColor ?? blendColor(foregroundColor: highlightedCandidateTextColor, backgroundColor: highlightedBackColor),
.font: labelFont,
.baselineOffset: baseOffset + (!vertical ? (font.pointSize - labelFont.pointSize) / 2.5 : 0)
]
private(set) lazy var commentAttrs: [NSAttributedString.Key: Any] = [
.foregroundColor: commentTextColor ?? candidateTextColor,
.font: commentFont,
.baselineOffset: baseOffset + (!vertical ? (font.pointSize - commentFont.pointSize) / 2.5 : 0)
]
private(set) lazy var commentHighlightedAttrs: [NSAttributedString.Key: Any] = [
.foregroundColor: highlightedCommentTextColor ?? highlightedCandidateTextColor,
.font: commentFont,
.baselineOffset: baseOffset + (!vertical ? (font.pointSize - commentFont.pointSize) / 2.5 : 0)
]
private(set) lazy var preeditAttrs: [NSAttributedString.Key: Any] = [
.foregroundColor: textColor,
.font: font,
.baselineOffset: baseOffset
]
private(set) lazy var preeditHighlightedAttrs: [NSAttributedString.Key: Any] = [
.foregroundColor: highlightedTextColor,
.font: font,
.baselineOffset: baseOffset
]
private(set) lazy var firstParagraphStyle: NSParagraphStyle = {
let style = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
style.paragraphSpacing = linespace / 2
style.paragraphSpacingBefore = preeditLinespace / 2 + hilitedCornerRadius / 2
return style as NSParagraphStyle
}()
private(set) lazy var paragraphStyle: NSParagraphStyle = {
let style = NSParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
style.paragraphSpacing = linespace / 2
style.paragraphSpacingBefore = linespace / 2
return style as NSParagraphStyle
}()
private(set) lazy var preeditParagraphStyle: NSParagraphStyle = {
let style = NSMutableParagraphStyle.default.mutableCopy() as! NSMutableParagraphStyle
style.paragraphSpacing = preeditLinespace / 2 + hilitedCornerRadius / 2
style.lineSpacing = linespace
return style as NSParagraphStyle
}()
private(set) lazy var edgeInset: NSSize = if self.vertical {
NSSize(width: borderHeight + cornerRadius, height: borderWidth + cornerRadius)
} else {
NSSize(width: borderWidth + cornerRadius, height: borderHeight + cornerRadius)
}
private(set) lazy var borderLineWidth: CGFloat = min(borderHeight, borderWidth)
private(set) var candidateFormat: String {
get {
_candidateFormat
} set {
var newTemplate = newValue
if newTemplate.contains(/%@/) {
newTemplate.replace(/%@/, with: "[candidate] [comment]")
}
if newTemplate.contains(/%c/) {
newTemplate.replace(/%c/, with: "[label]")
}
_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" }
vertical ?= config.getString("style/text_orientation").map { $0 == "vertical" }
inlinePreedit ?= config.getBool("style/inline_preedit")
inlineCandidate ?= config.getBool("style/inline_candidate")
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")
alpha ?= config.getDouble("style/alpha").map { min(1, max(0, $0)) }
cornerRadius ?= config.getDouble("style/corner_radius")
hilitedCornerRadius ?= config.getDouble("style/hilited_corner_radius")
surroundingExtraExpansion ?= config.getDouble("style/surrounding_extra_expansion")
borderHeight ?= config.getDouble("style/border_height")
borderWidth ?= config.getDouble("style/border_width")
linespace ?= config.getDouble("style/line_spacing")
preeditLinespace ?= config.getDouble("style/spacing")
baseOffset ?= config.getDouble("style/base_offset")
shadowSize ?= config.getDouble("style/shadow_size").map { max(0, $0) }
var fontName = config.getString("style/font_face")
var fontSize = config.getDouble("style/font_point")
var labelFontName = config.getString("style/label_font_face")
var labelFontSize = config.getDouble("style/label_font_point")
var commentFontName = config.getString("style/comment_font_face")
var commentFontSize = config.getDouble("style/comment_font_point")
let colorSchemeOption = dark ? "style/color_scheme_dark" : "style/color_scheme"
if let colorScheme = config.getString(colorSchemeOption) {
if colorScheme != "native" {
native = false
let prefix = "preset_color_schemes/\(colorScheme)"
colorSpace = .from(name: config.getString("\(prefix)/color_space") ?? "")
backgroundColor ?= config.getColor("\(prefix)/back_color", inSpace: colorSpace)
highlightedPreeditColor = config.getColor("\(prefix)/hilited_back_color", inSpace: colorSpace)
highlightedBackColor = config.getColor("\(prefix)/hilited_candidate_back_color", inSpace: colorSpace) ?? highlightedPreeditColor
preeditBackgroundColor = config.getColor("\(prefix)/preedit_back_color", inSpace: colorSpace)
candidateBackColor = config.getColor("\(prefix)/candidate_back_color", inSpace: colorSpace)
borderColor = config.getColor("\(prefix)/border_color", inSpace: colorSpace)
textColor ?= config.getColor("\(prefix)/text_color", inSpace: colorSpace)
highlightedTextColor = config.getColor("\(prefix)/hilited_text_color", inSpace: colorSpace) ?? textColor
candidateTextColor = config.getColor("\(prefix)/candidate_text_color", inSpace: colorSpace) ?? textColor
highlightedCandidateTextColor = config.getColor("\(prefix)/hilited_candidate_text_color", inSpace: colorSpace) ?? highlightedTextColor
candidateLabelColor = config.getColor("\(prefix)/label_color", inSpace: colorSpace)
highlightedCandidateLabelColor = config.getColor("\(prefix)/hilited_candidate_label_color", inSpace: colorSpace)
commentTextColor = config.getColor("\(prefix)/comment_text_color", inSpace: colorSpace)
highlightedCommentTextColor = config.getColor("\(prefix)/hilited_comment_text_color", inSpace: colorSpace)
// the following per-color-scheme configurations, if exist, will
// override configurations with the same name under the global 'style'
// section
linear ?= config.getString("\(prefix)/candidate_list_layout").map { $0 == "linear" }
vertical ?= config.getString("\(prefix)/text_orientation").map { $0 == "vertical" }
inlinePreedit ?= config.getBool("\(prefix)/inline_preedit")
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")
labelFontName ?= config.getString("\(prefix)/label_font_face")
labelFontSize ?= config.getDouble("\(prefix)/label_font_point")
commentFontName ?= config.getString("\(prefix)/comment_font_face")
commentFontSize ?= config.getDouble("\(prefix)/comment_font_point")
alpha ?= config.getDouble("\(prefix)/alpha").map { max(0, min(1, $0)) }
cornerRadius ?= config.getDouble("\(prefix)/corner_radius")
hilitedCornerRadius ?= config.getDouble("\(prefix)/hilited_corner_radius")
surroundingExtraExpansion ?= config.getDouble("\(prefix)/surrounding_extra_expansion")
borderHeight ?= config.getDouble("\(prefix)/border_height")
borderWidth ?= config.getDouble("\(prefix)/border_width")
linespace ?= config.getDouble("\(prefix)/line_spacing")
preeditLinespace ?= config.getDouble("\(prefix)/spacing")
baseOffset ?= config.getDouble("\(prefix)/base_offset")
shadowSize ?= config.getDouble("\(prefix)/shadow_size").map { max(0, $0) }
}
} else {
available = false
}
fonts = decodeFonts(from: fontName)
self.fontSize = fontSize
labelFonts = decodeFonts(from: labelFontName ?? fontName)
self.labelFontSize = labelFontSize
commentFonts = decodeFonts(from: commentFontName ?? fontName)
self.commentFontSize = commentFontSize
}
}
private extension SquirrelTheme {
func combineFonts(_ fonts: [NSFont], size: CGFloat?) -> NSFont? {
if fonts.count == 0 { return nil }
if fonts.count == 1 {
if let size = size {
return fonts[0].withSize(size)
} else {
return fonts[0]
}
}
let attribute = [NSFontDescriptor.AttributeName.cascadeList: fonts[1...].map { $0.fontDescriptor } ]
let fontDescriptor = fonts[0].fontDescriptor.addingAttributes(attribute)
return NSFont.init(descriptor: fontDescriptor, size: size ?? fonts[0].pointSize)
}
func decodeFonts(from fontString: String?) -> [NSFont] {
guard let fontString = fontString else { return [] }
var seenFontFamilies = Set<String>()
let fontStrings = fontString.split(separator: ",")
var fonts = [NSFont]()
for string in fontStrings {
if let matchedFontName = try? /^\s*(.+)-([^-]+)\s*$/.firstMatch(in: string) {
let family = String(matchedFontName.output.1)
let style = String(matchedFontName.output.2)
if seenFontFamilies.contains(family) { continue }
let fontDescriptor = NSFontDescriptor(fontAttributes: [.family: family, .face: style])
if let font = NSFont(descriptor: fontDescriptor, size: Self.defaultFontSize) {
fonts.append(font)
seenFontFamilies.insert(family)
continue
}
}
let fontName = string.trimmingCharacters(in: .whitespaces)
if seenFontFamilies.contains(fontName) { continue }
let fontDescriptor = NSFontDescriptor(fontAttributes: [.name: fontName])
if let font = NSFont(descriptor: fontDescriptor, size: Self.defaultFontSize) {
fonts.append(font)
seenFontFamilies.insert(fontName)
continue
}
}
return fonts
}
func blendColor(foregroundColor: NSColor, backgroundColor: NSColor?) -> NSColor {
let foregroundColor = foregroundColor.usingColorSpace(NSColorSpace.deviceRGB)!
let backgroundColor = (backgroundColor ?? NSColor.gray).usingColorSpace(NSColorSpace.deviceRGB)!
func blend(foreground: CGFloat, background: CGFloat) -> CGFloat {
return (foreground * 2 + background) / 3
}
return NSColor(deviceRed: blend(foreground: foregroundColor.redComponent, background: backgroundColor.redComponent),
green: blend(foreground: foregroundColor.greenComponent, background: backgroundColor.greenComponent),
blue: blend(foreground: foregroundColor.blueComponent, background: backgroundColor.blueComponent),
alpha: blend(foreground: foregroundColor.alphaComponent, background: backgroundColor.alphaComponent))
}
}