diff --git a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSView.swift b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSView.swift new file mode 100644 index 00000000..f9c5d4e0 --- /dev/null +++ b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSView.swift @@ -0,0 +1,396 @@ +// (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 +import SwiftExtension + +// MARK: - NSAlert + +public extension NSAlert { + func beginSheetModal(at window: NSWindow?, completionHandler handler: @escaping (NSApplication.ModalResponse) -> Void) { + if let window = window ?? NSApp.keyWindow { + beginSheetModal(for: window, completionHandler: handler) + } else { + handler(runModal()) + } + } +} + +// MARK: - NSOpenPanel + +public extension NSOpenPanel { + func beginSheetModal(at window: NSWindow?, completionHandler handler: @escaping (NSApplication.ModalResponse) -> Void) { + if let window = window ?? NSApp.keyWindow { + beginSheetModal(for: window, completionHandler: handler) + } else { + handler(runModal()) + } + } +} + +// MARK: - NSButton + +public extension NSButton { + convenience init(verbatim title: String, target: AnyObject?, action: Selector?) { + self.init() + self.title = title + self.target = target + self.action = action + bezelStyle = .rounded + } + + convenience init(_ title: String, target: AnyObject?, action: Selector?) { + self.init(verbatim: title.localized, target: target, action: action) + } +} + +// MARK: - Convenient Constructor for NSEdgeInsets. + +public extension NSEdgeInsets { + static func new(all: CGFloat? = nil, top: CGFloat? = nil, bottom: CGFloat? = nil, left: CGFloat? = nil, right: CGFloat? = nil) -> NSEdgeInsets { + NSEdgeInsets(top: top ?? all ?? 0, left: left ?? all ?? 0, bottom: bottom ?? all ?? 0, right: right ?? all ?? 0) + } +} + +// MARK: - Constrains and Box Container Modifier. + +public extension NSView { + @discardableResult func makeSimpleConstraint( + _ attribute: NSLayoutConstraint.Attribute, + relation: NSLayoutConstraint.Relation, + value: CGFloat? + ) -> NSView { + guard let value = value else { return self } + translatesAutoresizingMaskIntoConstraints = false + let widthConstraint = NSLayoutConstraint( + item: self, attribute: attribute, relatedBy: relation, toItem: nil, + attribute: NSLayoutConstraint.Attribute.notAnAttribute, multiplier: 1, constant: value + ) + addConstraint(widthConstraint) + return self + } + + func boxed(title: String = "") -> NSBox { + let maxDimension = fittingSize + let result = NSBox() + result.title = title.localized + if result.title.isEmpty { + result.titlePosition = .noTitle + } + let minWidth = Swift.max(maxDimension.width + 12, result.intrinsicContentSize.width) + let minHeight = Swift.max(maxDimension.height + result.titleRect.height + 14, result.intrinsicContentSize.height) + result.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: minWidth) + result.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: minHeight) + result.contentView = self + if let self = self as? NSStackView, self.orientation == .horizontal { + self.spacing = 0 + } + return result + } +} + +// MARK: - Stacks + +public extension NSStackView { + var requiresConstraintBasedLayout: Bool { + true + } + + static func buildSection( + _ orientation: NSUserInterfaceLayoutOrientation = .vertical, + width: CGFloat? = nil, + withDividers: Bool = true, + @ArrayBuilder views: () -> [NSView?] + ) -> NSStackView? { + let viewsRendered = views().compactMap { + // 下述註解是用來協助偵錯的。 + // $0?.wantsLayer = true + // $0?.layer?.backgroundColor = NSColor.red.cgColor + $0 + } + guard !viewsRendered.isEmpty else { return nil } + var itemWidth = width + var splitterDelta: CGFloat = 4 + splitterDelta = withDividers ? splitterDelta : 0 + if let width = width, orientation == .horizontal, viewsRendered.count > 0 { + itemWidth = (width - splitterDelta) / CGFloat(viewsRendered.count) - 6 + } + func giveViews() -> [NSView?] { viewsRendered } + let result = build(orientation, divider: withDividers, width: itemWidth, views: giveViews)? + .withInsets(.new(all: 4)) + return result + } + + static func build( + _ orientation: NSUserInterfaceLayoutOrientation, + divider: Bool = false, + width: CGFloat? = nil, + height: CGFloat? = nil, + insets: NSEdgeInsets? = nil, + @ArrayBuilder views: () -> [NSView?] + ) -> NSStackView? { + let result = views().compactMap { + $0? + .makeSimpleConstraint(.width, relation: .equal, value: width) + .makeSimpleConstraint(.height, relation: .equal, value: height) + } + guard !result.isEmpty else { return nil } + return result.stack(orientation, divider: divider)?.withInsets(insets) + } + + func withInsets(_ newValue: NSEdgeInsets?) -> NSStackView { + edgeInsets = newValue ?? edgeInsets + return self + } +} + +public extension Array where Element == NSView { + func stack( + _ orientation: NSUserInterfaceLayoutOrientation, + divider: Bool = false, + insets: NSEdgeInsets? = nil + ) -> NSStackView? { + guard !isEmpty else { return nil } + let outerStack = NSStackView() + if #unavailable(macOS 10.11) { + outerStack.hasEqualSpacing = true + } else { + outerStack.distribution = .equalSpacing + } + outerStack.orientation = orientation + + if #unavailable(macOS 10.10) { + outerStack.spacing = Swift.max(1, outerStack.spacing) - 1 + } + + outerStack.setHuggingPriority(.fittingSizeCompression, for: .horizontal) + outerStack.setHuggingPriority(.fittingSizeCompression, for: .vertical) + + forEach { subView in + if divider, !outerStack.views.isEmpty { + let divider = NSView() + divider.wantsLayer = true + divider.layer?.backgroundColor = NSColor.gray.withAlphaComponent(0.2).cgColor + switch orientation { + case .horizontal: + divider.makeSimpleConstraint(.width, relation: .equal, value: 1) + case .vertical: + divider.makeSimpleConstraint(.height, relation: .equal, value: 1) + @unknown default: break + } + divider.translatesAutoresizingMaskIntoConstraints = false + outerStack.addView(divider, in: orientation == .horizontal ? .leading : .top) + } + subView.layoutSubtreeIfNeeded() + switch orientation { + case .horizontal: + subView.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: subView.intrinsicContentSize.height) + subView.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: subView.intrinsicContentSize.width) + case .vertical: + subView.makeSimpleConstraint(.width, relation: .greaterThanOrEqual, value: subView.intrinsicContentSize.width) + subView.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: subView.intrinsicContentSize.height) + @unknown default: break + } + subView.setContentCompressionResistancePriority(.defaultHigh, for: .horizontal) + subView.setContentCompressionResistancePriority(.defaultHigh, for: .vertical) + subView.translatesAutoresizingMaskIntoConstraints = false + outerStack.addView(subView, in: orientation == .horizontal ? .leading : .top) + } + + switch orientation { + case .horizontal: + outerStack.alignment = .centerY + case .vertical: + outerStack.alignment = .leading + @unknown default: break + } + return outerStack.withInsets(insets) + } +} + +// MARK: - Make NSAttributedString into Label + +public extension NSAttributedString { + func makeNSLabel(fixWidth: CGFloat? = nil) -> NSTextField { + let textField = NSTextField() + textField.attributedStringValue = self + textField.isEditable = false + textField.isBordered = false + textField.backgroundColor = .clear + if let fixWidth = fixWidth { + textField.preferredMaxLayoutWidth = fixWidth + } + return textField + } +} + +// MARK: - Make String into Label + +public extension String { + func makeNSLabel(descriptive: Bool = false, localized: Bool = true, fixWidth: CGFloat? = nil) -> NSTextField { + let rawAttributedString = NSMutableAttributedString(string: localized ? self.localized : self) + rawAttributedString.addAttributes([.kern: 0], range: .init(location: 0, length: rawAttributedString.length)) + let textField = rawAttributedString.makeNSLabel(fixWidth: fixWidth) + if descriptive { + if #available(macOS 10.10, *) { + textField.textColor = .secondaryLabelColor + } else { + textField.textColor = .textColor.withAlphaComponent(0.55) + } + textField.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + } + return textField + } +} + +// MARK: - NSTabView + +public extension NSTabView { + struct TabPage { + public let title: String + public let view: NSView + + public init?(title: String, view: NSView?) { + self.title = title + guard let view = view else { return nil } + self.view = view + } + + public init(title: String, view: NSView) { + self.title = title + self.view = view + } + + public init?(title: String, @ArrayBuilder views: () -> [NSView?]) { + self.title = title + let viewsRendered = views() + guard !viewsRendered.isEmpty else { return nil } + func giveViews() -> [NSView?] { viewsRendered } + let result = NSStackView.build(.vertical, insets: .new(all: 14, top: 0), views: giveViews) + guard let result = result else { return nil } + view = result + } + } + + static func build( + @ArrayBuilder pages: () -> [TabPage?] + ) -> NSTabView? { + let tabPages = pages().compactMap { $0 } + guard !tabPages.isEmpty else { return nil } + let finalTabView = NSTabView() + tabPages.forEach { currentPage in + finalTabView.addTabViewItem({ + let currentItem = NSTabViewItem(identifier: UUID()) + currentItem.label = currentPage.title.localized + let stacked = NSStackView.build(.vertical) { + currentPage.view + } + stacked?.alignment = .centerX + currentItem.view = stacked + return currentItem + }()) + } + return finalTabView + } +} + +// MARK: - NSMenu + +public extension NSMenu { + @discardableResult func appendItems(_ target: AnyObject? = nil, @ArrayBuilder items: () -> [NSMenuItem?]) -> NSMenu { + let theItems = items() + for currentItem in theItems { + guard let currentItem = currentItem else { continue } + addItem(currentItem) + guard let target = target else { continue } + currentItem.target = target + currentItem.submenu?.propagateTarget(target) + } + return self + } + + @discardableResult func propagateTarget(_ obj: AnyObject?) -> NSMenu { + for currentItem in items { + currentItem.target = obj + currentItem.submenu?.propagateTarget(obj) + } + return self + } + + static func buildSubMenu(verbatim: String?, @ArrayBuilder items: () -> [NSMenuItem?]) -> NSMenuItem? { + guard let verbatim = verbatim, !verbatim.isEmpty else { return nil } + let newItem = NSMenu.Item(verbatim: verbatim) + newItem?.submenu = .init(title: verbatim).appendItems(items: items) + return newItem + } + + static func buildSubMenu(_ title: String?, @ArrayBuilder items: () -> [NSMenuItem?]) -> NSMenuItem? { + guard let title = title?.localized, !title.isEmpty else { return nil } + return buildSubMenu(verbatim: title, items: items) + } + + typealias Item = NSMenuItem +} + +public extension Array where Element == NSMenuItem? { + func propagateTarget(_ obj: AnyObject?) { + forEach { currentItem in + guard let currentItem = currentItem else { return } + currentItem.target = obj + currentItem.submenu?.propagateTarget(obj) + } + } +} + +public extension NSMenuItem { + convenience init?(verbatim: String?) { + guard let verbatim = verbatim, !verbatim.isEmpty else { return nil } + self.init(title: verbatim, action: nil, keyEquivalent: "") + } + + convenience init?(_ title: String?) { + guard let title = title?.localized, !title.isEmpty else { return nil } + self.init(verbatim: title) + } + + @discardableResult func hotkey(_ keyEquivalent: String, mask: NSEvent.ModifierFlags? = nil) -> NSMenuItem { + keyEquivalentModifierMask = mask ?? keyEquivalentModifierMask + self.keyEquivalent = keyEquivalent + return self + } + + @discardableResult func state(_ givenState: Bool) -> NSMenuItem { + state = givenState ? .on : .off + return self + } + + @discardableResult func act(_ action: Selector) -> NSMenuItem { + self.action = action + return self + } + + @discardableResult func nulled(_ condition: Bool) -> NSMenuItem? { + condition ? nil : self + } + + @discardableResult func mask(_ flags: NSEvent.ModifierFlags) -> NSMenuItem { + keyEquivalentModifierMask = flags + return self + } + + @discardableResult func represent(_ object: Any?) -> NSMenuItem { + representedObject = object + return self + } + + @discardableResult func tag(_ givenTag: Int?) -> NSMenuItem { + guard let givenTag = givenTag else { return self } + tag = givenTag + return self + } +} diff --git a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowController.swift b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowController.swift index 8e10fa81..94296827 100644 --- a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowController.swift +++ b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowController.swift @@ -48,12 +48,19 @@ public extension NSWindowController { } public extension NSWindow { + @discardableResult func callAlert(title: String, text: String? = nil) -> NSApplication.ModalResponse { + (self as NSWindow?).callAlert(title: title, text: text) + } +} + +public extension NSWindow? { @discardableResult func callAlert(title: String, text: String? = nil) -> NSApplication.ModalResponse { let alert = NSAlert() alert.messageText = title if let text = text { alert.informativeText = text } alert.addButton(withTitle: NSLocalizedString("OK", comment: "")) var result: NSApplication.ModalResponse = .alertFirstButtonReturn + guard let self = self else { return alert.runModal() } alert.beginSheetModal(for: self) { theResponce in result = theResponce } diff --git a/Packages/vChewing_Shared/Sources/Shared/UserDef.swift b/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDef.swift similarity index 99% rename from Packages/vChewing_Shared/Sources/Shared/UserDef.swift rename to Packages/vChewing_Shared/Sources/Shared/UserDef/UserDef.swift index 819250b2..ec1db823 100644 --- a/Packages/vChewing_Shared/Sources/Shared/UserDef.swift +++ b/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDef.swift @@ -20,7 +20,6 @@ public enum UserDef: String, CaseIterable, Identifiable { public struct MetaData { public var userDef: UserDef public var shortTitle: String? - public var control: AnyObject? public var prompt: String? public var inlinePrompt: String? public var popupPrompt: String? diff --git a/Packages/vChewing_Shared/Sources/Shared/UserDefRenderable.swift b/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderable.swift similarity index 100% rename from Packages/vChewing_Shared/Sources/Shared/UserDefRenderable.swift rename to Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderable.swift diff --git a/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderableCocoa.swift b/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderableCocoa.swift new file mode 100644 index 00000000..f1d60a2c --- /dev/null +++ b/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderableCocoa.swift @@ -0,0 +1,275 @@ +// (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. + +// 這其實是 UserDefRenderable 的另一個版本,但用的是 AppKit 而非 SwiftUI。 + +import AppKit +import CocoaExtension +import Foundation +import IMKUtils + +public class UserDefRenderableCocoa: NSObject, Identifiable { + public let def: UserDef + public var id: String { def.rawValue } + public var optionsLocalized: [(Int, String)?] + private var optionsLocalizedAsIdentifiables: [(String, String)?] = [] // 非 Int 型資料專用(例:鍵盤佈局選擇器)。 + public var inlineDescriptionLocalized: String? + public var hideTitle: Bool = false + public var mainViewOverride: (() -> NSView?)? + public var currentControl: NSControl? + public var tinySize: Bool = false + + public init(def: UserDef) { + self.def = def + if let rawOptions = def.metaData?.options, !rawOptions.isEmpty { + var newOptions: [Int: String] = [:] + rawOptions.forEach { key, value in + newOptions[key] = value.localized + } + optionsLocalized = rawOptions.sorted(by: { $0.key < $1.key }) + } else { + optionsLocalized = [] + } + + var objOptions = [(String, String)?]() + var intOptions = [(Int, String)?]() + checkDef: switch def { + case .kAlphanumericalKeyboardLayout: + IMKHelper.allowedAlphanumericalTISInputSources.forEach { currentTIS in + objOptions.append((currentTIS.identifier, currentTIS.vChewingLocalizedName)) + } + optionsLocalizedAsIdentifiables = objOptions + case .kBasicKeyboardLayout: + IMKHelper.allowedBasicLayoutsAsTISInputSources.forEach { currentTIS in + guard let currentTIS = currentTIS else { + objOptions.append(nil) + return + } + objOptions.append((currentTIS.identifier, currentTIS.vChewingLocalizedName)) + } + optionsLocalizedAsIdentifiables = objOptions + case .kKeyboardParser: + KeyboardParser.allCases.forEach { currentParser in + if [7, 100].contains(currentParser.rawValue) { intOptions.append(nil) } + intOptions.append((currentParser.rawValue, currentParser.localizedMenuName)) + } + optionsLocalized = intOptions + default: break checkDef + } + + super.init() + guard let metaData = def.metaData else { + inlineDescriptionLocalized = nil + return + } + var stringStack = [String]() + if let promptText = metaData.inlinePrompt?.localized, !promptText.isEmpty { + stringStack.append(promptText) + } + if let descText = metaData.description?.localized, !descText.isEmpty { + stringStack.append(descText) + } + if metaData.minimumOS > 10.9 { + var strOSReq = " " + strOSReq += String( + format: "This feature requires macOS %@ and above.".localized, arguments: ["12.0"] + ) + stringStack.append(strOSReq) + } + currentControl = renderFunctionControl() + guard !stringStack.isEmpty else { + inlineDescriptionLocalized = nil + return + } + inlineDescriptionLocalized = stringStack.joined(separator: "\n") + } +} + +public extension UserDefRenderableCocoa { + func render(fixWidth fixedWith: CGFloat? = nil) -> NSView? { + let result: NSStackView? = NSStackView.build(.vertical) { + renderMainLine(fixedWidth: fixedWith) + renderDescription(fixedWidth: fixedWith) + } + result?.makeSimpleConstraint(.width, relation: .equal, value: fixedWith) + return result + } + + func renderDescription(fixedWidth: CGFloat? = nil) -> NSTextField? { + guard let text = inlineDescriptionLocalized else { return nil } + let textField = text.makeNSLabel(descriptive: true) + if #available(macOS 10.10, *), tinySize { + textField.controlSize = .small + textField.font = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + } + textField.preferredMaxLayoutWidth = fixedWidth ?? 0 + return textField + } + + func renderMainLine(fixedWidth: CGFloat? = nil) -> NSView? { + if let mainViewOverride = mainViewOverride { + return mainViewOverride() + } + guard let control: NSView = currentControl ?? renderFunctionControl() else { return nil } + let controlWidth = control.fittingSize.width + let textLabel: NSTextField? = { + if !hideTitle, let strTitle = def.metaData?.shortTitle { + return strTitle.makeNSLabel() + } + return nil + }() + let result = NSStackView.build(.horizontal) { + if !hideTitle, let textlabel = textLabel { + textlabel + NSView() + } + control + } + if let fixedWidth = fixedWidth { + textLabel?.preferredMaxLayoutWidth = fixedWidth - controlWidth + } + textLabel?.sizeToFit() + return result + } + + private func renderFunctionControl() -> NSControl? { + var result: NSControl? { + switch def.dataType { + case .string where def == .kCandidateKeys: + let comboBox = NSComboBox() + comboBox.makeSimpleConstraint(.width, relation: .equal, value: 128) + comboBox.font = NSFont.systemFont(ofSize: 12) + comboBox.intercellSpacing = NSSize(width: 0.0, height: 10.0) + comboBox.addItems(withObjectValues: CandidateKey.suggestions) + comboBox.bind( + .value, + to: NSUserDefaultsController.shared, + withKeyPath: "values.\(def.rawValue)" + ) + return comboBox + case .bool where optionsLocalized.isEmpty: + let checkBox: NSControl + if #unavailable(macOS 10.15) { + checkBox = NSButton() + (checkBox as? NSButton)?.setButtonType(.switch) + (checkBox as? NSButton)?.title = "" + } else { + checkBox = NSSwitch() + checkBox.controlSize = .mini + } + checkBox.bind( + .value, + to: NSUserDefaultsController.shared, + withKeyPath: "values.\(def.rawValue)", + options: [.continuouslyUpdatesValue: true] + ) + + // 特殊情形開始:部分控件有啟用條件,條件不滿足則變灰。 + checkDef: switch def { + case .kAlwaysExpandCandidateWindow: + checkBox.bind( + .enabled, + to: NSUserDefaultsController.shared, + withKeyPath: "values.\(UserDef.kCandidateWindowShowOnlyOneLine.rawValue)", + options: [ + .valueTransformerName: NSValueTransformerName.negateBooleanTransformerName, + ] + ) + case .kUseDynamicCandidateWindowOrigin: + checkBox.bind( + .enabled, + to: NSUserDefaultsController.shared, + withKeyPath: "values.\(UserDef.kUseRearCursorMode.rawValue)", + options: [ + .valueTransformerName: NSValueTransformerName.negateBooleanTransformerName, + ] + ) + default: break checkDef + } + // 特殊情形結束 + + return checkBox + case .integer, .double, + .bool where !optionsLocalized.isEmpty, + .string where !optionsLocalized.isEmpty, + .string where !optionsLocalizedAsIdentifiables.isEmpty: + let dropMenu: NSMenu = .init() + let btnPopup = NSPopUpButton() + var itemShouldBeChosen: NSMenuItem? + if !optionsLocalizedAsIdentifiables.isEmpty { + btnPopup.bind( + .selectedObject, + to: NSUserDefaultsController.shared, + withKeyPath: "values.\(def.rawValue)", + options: [.continuouslyUpdatesValue: true] + ) + optionsLocalizedAsIdentifiables.forEach { entity in + guard let obj = entity?.0, let title = entity?.1.localized else { + dropMenu.addItem(.separator()) + return + } + let newItem = NSMenuItem(title: title, action: nil, keyEquivalent: "") + newItem.representedObject = .init(obj) + if obj == UserDefaults.current.object(forKey: def.rawValue) as? String { + itemShouldBeChosen = newItem + } + dropMenu.addItem(newItem) + } + } else { + btnPopup.bind( + .selectedTag, + to: NSUserDefaultsController.shared, + withKeyPath: "values.\(def.rawValue)", + options: [.continuouslyUpdatesValue: true] + ) + optionsLocalized.forEach { entity in + guard let tag = entity?.0, let title = entity?.1.localized else { + dropMenu.addItem(.separator()) + return + } + let newItem = NSMenuItem(title: title, action: nil, keyEquivalent: "") + newItem.tag = tag + if tag == UserDefaults.current.integer(forKey: def.rawValue) { + itemShouldBeChosen = newItem + } + if Double(tag) == UserDefaults.current.double(forKey: def.rawValue) { + itemShouldBeChosen = newItem + } + dropMenu.addItem(newItem) + } + } + btnPopup.menu = dropMenu + btnPopup.font = NSFont.systemFont(ofSize: 12) + btnPopup.setFrameSize(btnPopup.fittingSize) + btnPopup.select(itemShouldBeChosen) + return btnPopup + case .array, .dictionary, .other: return nil + default: return nil + } + } + if #available(macOS 10.10, *), tinySize { + result?.controlSize = .small + return result?.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: Swift.max(14, result?.fittingSize.height ?? 14)) as? NSControl + } + return result?.makeSimpleConstraint(.height, relation: .greaterThanOrEqual, value: Swift.max(16, result?.fittingSize.height ?? 16)) as? NSControl + } +} + +// MARK: - External Extensions. + +public extension UserDef { + func render(fixWidth: CGFloat? = nil, extraOps: ((inout UserDefRenderableCocoa) -> Void)? = nil) -> NSView? { + var renderable = toCocoaRenderable() + extraOps?(&renderable) + return renderable.render(fixWidth: fixWidth) + } + + func toCocoaRenderable() -> UserDefRenderableCocoa { + .init(def: self) + } +}