diff --git a/CotEditor/Sources/Document Window/Accessory Views/CharacterInspectorView.swift b/CotEditor/Sources/Document Window/Accessory Views/CharacterInspectorView.swift index 5f0d77ed2..7111b2aed 100644 --- a/CotEditor/Sources/Document Window/Accessory Views/CharacterInspectorView.swift +++ b/CotEditor/Sources/Document Window/Accessory Views/CharacterInspectorView.swift @@ -8,7 +8,7 @@ // // --------------------------------------------------------------------------- // -// © 2021-2025 1024jp +// © 2021-2026 1024jp // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -28,51 +28,56 @@ import CharacterInfo struct CharacterInspectorView: View { - var info: CharacterInfo + var character: Character + + + init(_ character: Character) { + + self.character = character + } var body: some View { HStack(alignment: .top) { - CharacterView(info: self.info) + CharacterView(character: self.character) .frame(minWidth: 64) - CharacterDetailView(info: self.info) + CharacterDetailView(character: self.character) } - .padding(14) } } private struct CharacterDetailView: View { - var info: CharacterInfo + var character: Character var body: some View { VStack(alignment: .leading, spacing: 0) { - if let description = self.info.localizedDescription { + if let description = self.character.localizedDescription { Text(description) - .fontWeight(self.info.isComplex ? .regular : .semibold) + .fontWeight(self.character.isComplex ? .regular : .semibold) .textSelection(.enabled) } else { Text("Unknown", tableName: "CharacterInspector") .foregroundStyle(.secondary) } - if !self.info.isComplex { - ScalarDetailView(scalar: self.info.character.unicodeScalars.first!) + if !self.character.isComplex { + ScalarDetailView(scalar: self.character.unicodeScalars.first!) .controlSize(.small) .padding(.top, 4) } - if self.info.character.unicodeScalars.count > 1 { + if self.character.unicodeScalars.count > 1 { VStack(spacing: 0) { - ForEach(Array(self.info.character.unicodeScalars).enumerated(), id: \.offset) { _, scalar in + ForEach(Array(self.character.unicodeScalars).enumerated(), id: \.offset) { _, scalar in DisclosureGroup { HStack(alignment: .top) { let character = Character(scalar) - let pictureCharacter = CharacterInfo(character: character).pictureCharacter + let pictureCharacter = character.pictureCharacter Text(String(pictureCharacter ?? character)) .font(.system(size: 28, design: .serif)) @@ -204,7 +209,7 @@ private struct CharacterView: NSViewRepresentable { typealias NSViewType = NSTextField - var info: CharacterInfo + var character: Character private let fontSize: CGFloat = 64 @@ -223,8 +228,8 @@ private struct CharacterView: NSViewRepresentable { func updateNSView(_ nsView: NSTextField, context: Context) { - nsView.stringValue = String(self.info.pictureCharacter ?? self.info.character) - nsView.textColor = (self.info.pictureCharacter != nil) ? .tertiaryLabelColor : .labelColor + nsView.stringValue = String(self.character.pictureCharacter ?? self.character) + nsView.textColor = (self.character.pictureCharacter != nil) ? .tertiaryLabelColor : .labelColor } @@ -247,11 +252,11 @@ private struct DeprecatedBadge: View { } -private extension CharacterInfo { +private extension Character { var localizedDescription: String? { - let unicodes = self.character.unicodeScalars + let unicodes = self.unicodeScalars if self.isComplex { return String(localized: "", table: "CharacterInspector", @@ -272,30 +277,30 @@ private extension CharacterInfo { // MARK: - Preview #Preview("𓆏") { - CharacterInspectorView(info: CharacterInfo(character: "𓆏")) + CharacterInspectorView("𓆏") } #Preview("\\n") { - CharacterInspectorView(info: CharacterInfo(character: "\n")) + CharacterInspectorView("\n") } #Preview("ơ̟̤̖̗͖͇̍͋̀͆̓́͞͡") { - CharacterInspectorView(info: CharacterInfo(character: "ơ̟̤̖̗͖͇̍͋̀͆̓́͞͡")) + CharacterInspectorView("ơ̟̤̖̗͖͇̍͋̀͆̓́͞͡") } #Preview("✔︎") { - CharacterInspectorView(info: CharacterInfo(character: "✔︎")) + CharacterInspectorView("✔︎") .frame(height: 240, alignment: .top) } #Preview("🏴‍☠️") { - CharacterInspectorView(info: CharacterInfo(character: "🏴‍☠️")) + CharacterInspectorView("🏴‍☠️") } #Preview("🇦🇦") { - CharacterInspectorView(info: CharacterInfo(character: "🇦🇦")) + CharacterInspectorView("🇦🇦") } #Preview("deprecated") { - CharacterInspectorView(info: CharacterInfo(character: "ឣ")) + CharacterInspectorView("ឣ") } diff --git a/CotEditor/Sources/Document Window/Content View/EditorTextViewController.swift b/CotEditor/Sources/Document Window/Content View/EditorTextViewController.swift index 404226ec5..8cf244786 100644 --- a/CotEditor/Sources/Document Window/Content View/EditorTextViewController.swift +++ b/CotEditor/Sources/Document Window/Content View/EditorTextViewController.swift @@ -404,10 +404,10 @@ final class EditorTextViewController: NSViewController, NSServicesMenuRequestor, let character = self.textView.selectedString.first else { return assertionFailure() } - let characterInfo = CharacterInfo(character: character) + let view = NSHostingView(rootView: CharacterInspectorView(character).padding(14)) + view.frame.size = view.intrinsicContentSize let popoverController = DetachablePopoverViewController() - popoverController.view = NSHostingView(rootView: CharacterInspectorView(info: characterInfo)) - popoverController.view.frame.size = popoverController.view.intrinsicContentSize + popoverController.view = view let textView = self.textView let positioningRect = textView.boundingRect(for: textView.selectedRange)?.insetBy(dx: -4, dy: -4) ?? .zero diff --git a/Packages/EditorCore/Sources/CharacterInfo/CharacterInfo.swift b/Packages/EditorCore/Sources/CharacterInfo/Character.swift similarity index 53% rename from Packages/EditorCore/Sources/CharacterInfo/CharacterInfo.swift rename to Packages/EditorCore/Sources/CharacterInfo/Character.swift index 00bc93740..82a8b9453 100644 --- a/Packages/EditorCore/Sources/CharacterInfo/CharacterInfo.swift +++ b/Packages/EditorCore/Sources/CharacterInfo/Character.swift @@ -1,5 +1,5 @@ // -// CharacterInfo.swift +// Character.swift // CharacterInfo // // CotEditor @@ -9,7 +9,7 @@ // // --------------------------------------------------------------------------- // -// © 2015-2024 1024jp +// © 2015-2026 1024jp // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -24,50 +24,28 @@ // limitations under the License. // -public struct CharacterInfo: Sendable { - - // MARK: Public Properties - - public var character: Character - - - // MARK: Public Methods - - public init(character: Character) { - - self.character = character - } - +public extension Character { /// The representative character to display in the user interface. - public var pictureCharacter: Character? { + var pictureCharacter: Character? { - self.character.unicodeScalars.count == 1 // ignore CRLF - ? self.character.unicodeScalars.first?.pictureRepresentation.map(Character.init) + self.unicodeScalars.count == 1 // ignore CRLF + ? self.unicodeScalars.first?.pictureRepresentation.map(Character.init) : nil } /// Whether the character consists with multiple Unicode scalars. - public var isComplex: Bool { + var isComplex: Bool { - self.character.unicodeScalars.count > 1 && !self.isVariant + self.unicodeScalars.count > 1 && !self.isVariant } /// Whether the character is a single variant character. - public var isVariant: Bool { + var isVariant: Bool { - (self.character.unicodeScalars.count == 2 && - self.character.unicodeScalars.last?.variantDescription != nil) - } -} - - -extension CharacterInfo: CustomStringConvertible { - - public var description: String { - - String(self.character) + (self.unicodeScalars.count == 2 && + self.unicodeScalars.last?.variantDescription != nil) } } diff --git a/Packages/EditorCore/Tests/CharacterInfoTests/CharacterInfoTests.swift b/Packages/EditorCore/Tests/CharacterInfoTests/CharacterInfoTests.swift index ba225ae32..fb73e7383 100644 --- a/Packages/EditorCore/Tests/CharacterInfoTests/CharacterInfoTests.swift +++ b/Packages/EditorCore/Tests/CharacterInfoTests/CharacterInfoTests.swift @@ -9,7 +9,7 @@ // // --------------------------------------------------------------------------- // -// © 2015-2024 1024jp +// © 2015-2026 1024jp // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -31,39 +31,37 @@ struct CharacterInfoTests { @Test func singleCharacterWithVSInfo() { - let charInfo = CharacterInfo(character: "☺︎") + let character: Character = "☺︎" - #expect(charInfo.character == "☺︎") - #expect(!charInfo.isComplex) - #expect(charInfo.character.unicodeScalars.map(\.codePoint) == ["U+263A", "U+FE0E"]) - #expect(charInfo.character.unicodeScalars.map(\.name) == ["WHITE SMILING FACE", "VARIATION SELECTOR-15"]) + #expect(!character.isComplex) + #expect(character.unicodeScalars.map(\.codePoint) == ["U+263A", "U+FE0E"]) + #expect(character.unicodeScalars.map(\.name) == ["WHITE SMILING FACE", "VARIATION SELECTOR-15"]) } @Test func combiningCharacterInfo() { - let charInfo = CharacterInfo(character: "1️⃣") + let character: Character = "1️⃣" - #expect(charInfo.isComplex) - #expect(charInfo.character.unicodeScalars.map(\.codePoint) == ["U+0031", "U+FE0F", "U+20E3"]) + #expect(character.isComplex) + #expect(character.unicodeScalars.map(\.codePoint) == ["U+0031", "U+FE0F", "U+20E3"]) } @Test func nationalIndicatorInfo() { - let charInfo = CharacterInfo(character: "🇯🇵") + let character: Character = "🇯🇵" - #expect(charInfo.isComplex) - #expect(charInfo.character.unicodeScalars.map(\.codePoint) == ["U+1F1EF", "U+1F1F5"]) + #expect(character.isComplex) + #expect(character.unicodeScalars.map(\.codePoint) == ["U+1F1EF", "U+1F1F5"]) } @Test func controlCharacterInfo() { - let charInfo = CharacterInfo(character: " ") + let character: Character = " " - #expect(charInfo.character == " ") - #expect(charInfo.pictureCharacter == "␠") - #expect(charInfo.character.unicodeScalars.map(\.name) == ["SPACE"]) + #expect(character.pictureCharacter == "␠") + #expect(character.unicodeScalars.map(\.name) == ["SPACE"]) } }