forked from microsoft/fluentui-apple
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
[macOS] Multiline Pill Picker (microsoft#2121)
* First pass on MultilinePillPicker * Add disabling to AppKit wrapper and demo controller * Fix deprecated API * Use line width constant for padding * Remove @mainactor from WIP code * Comment updates
- Loading branch information
1 parent
1dfcb5f
commit fa970f4
Showing
8 changed files
with
226 additions
and
1 deletion.
There are no files selected for viewing
57 changes: 57 additions & 0 deletions
57
...luentUIDemo_macOS/FluentUITestViewControllers/TestMultilinePillPickerViewController.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
// | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
// | ||
|
||
import AppKit | ||
import FluentUI | ||
import SwiftUI | ||
|
||
class TestMultilinePillPickerViewController: NSViewController { | ||
override func loadView() { | ||
let containerView = NSStackView(frame: .zero) | ||
containerView.orientation = .vertical | ||
|
||
let pillPickerView = MultilinePillPickerView(labels: labels) { [weak self] index in | ||
self?.pillButtonPressed(index) | ||
} | ||
containerView.addView(pillPickerView, in: .center) | ||
self.pillPickerView = pillPickerView | ||
|
||
let checkbox = NSButton(checkboxWithTitle: "Enabled", target: self, action: #selector(toggleEnabled(_:))) | ||
checkbox.state = .on | ||
containerView.addView(checkbox, in: .center) | ||
|
||
view = containerView | ||
} | ||
|
||
@objc func toggleEnabled(_ sender: NSButton?) { | ||
pillPickerView?.isEnabled = sender?.state == .on | ||
} | ||
|
||
func pillButtonPressed(_ index: Int) { | ||
guard let window = NSApplication.shared.mainWindow else { | ||
print("No window -- selected index \(index)") | ||
return | ||
} | ||
let alert = NSAlert() | ||
alert.messageText = "Suggestion clicked" | ||
alert.informativeText = "\(labels[index])" | ||
alert.addButton(withTitle: "OK") | ||
alert.beginSheetModal(for: window) | ||
} | ||
|
||
private let labels: [String] = [ | ||
"One", | ||
"Two", | ||
"Three", | ||
"Four", | ||
"Five", | ||
"Six", | ||
"Seven", | ||
"Eight", | ||
"Nine", | ||
] | ||
|
||
private var pillPickerView: MultilinePillPickerView? | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
65 changes: 65 additions & 0 deletions
65
Sources/FluentUI_macOS/Components/MultilinePillPicker/MultilinePillPicker.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
// | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
// | ||
|
||
import SwiftUI | ||
|
||
/// This is a simple control for hosting multiple rows of pill buttons. At present, this control only | ||
/// supports a hard-coded two rows of elements, but more flexibility may be added in the future. | ||
public struct MultilinePillPicker: View { | ||
/// Creates a multiline pill picker. | ||
/// - Parameters: | ||
/// - labels: An array of labels to show in the picker. | ||
/// - action: An action to invoke when a pill is selected in the picker. Includes the index of the item being selected. | ||
public init(labels: [String], action: ((Int) -> Void)? = nil) { | ||
self.labels = labels | ||
self.action = action | ||
} | ||
|
||
public var body: some View { | ||
VStack(alignment: .leading, spacing: spacing) { | ||
// Bias towards the first row | ||
let midIndex = Int(ceil(Double(labels.count) / 2.0)) | ||
row(0..<midIndex) | ||
row(midIndex..<labels.count) | ||
} | ||
.frame(alignment: .center) | ||
.padding(lineWidth) | ||
} | ||
|
||
@ViewBuilder | ||
private func button(_ index: Int) -> some View { | ||
SwiftUI.Button(action: { | ||
action?(index) | ||
}, label: { | ||
Text(labels[index]) | ||
.padding(.vertical, paddingVertical) | ||
.padding(.horizontal, paddingHorizontal) | ||
.overlay( | ||
RoundedRectangle(cornerRadius: cornerRadius, style: .continuous) | ||
.stroke(Color(nsColor: Colors.primaryTint10), lineWidth: lineWidth) | ||
) | ||
}) | ||
.buttonStyle(.plain) | ||
} | ||
|
||
@ViewBuilder | ||
private func row(_ range: Range<Int>) -> some View { | ||
HStack(spacing: spacing) { | ||
ForEach(range, id: \.self) { index in | ||
button(index) | ||
} | ||
} | ||
} | ||
|
||
private let labels: [String] | ||
private let action: ((Int) -> Void)? | ||
|
||
// Constants | ||
private let cornerRadius: CGFloat = 6.0 | ||
private let lineWidth: CGFloat = 1.0 | ||
private let paddingHorizontal: CGFloat = 8.0 | ||
private let paddingVertical: CGFloat = 4.0 | ||
private let spacing: CGFloat = 4.0 | ||
} |
54 changes: 54 additions & 0 deletions
54
Sources/FluentUI_macOS/Components/MultilinePillPicker/MultilinePillPickerView.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
// | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
// | ||
|
||
import AppKit | ||
import SwiftUI | ||
|
||
/// This is a work-in-progress control for hosting multiple rows of pill buttons. At present, this control | ||
/// only supports a hard-coded two rows of elements. | ||
@objc(MSFMultilinePillPickerView) | ||
public final class MultilinePillPickerView: ControlHostingView { | ||
/// Creates a multiline pill picker. | ||
/// - Parameters: | ||
/// - labels: An array of labels to show in the picker. | ||
/// - action: An action to invoke when a pill is selected in the picker. Includes the index of the item being selected. | ||
@objc(initWithLabels:action:) | ||
@MainActor public init(labels: [String], action: (@MainActor (Int) -> Void)? = nil) { | ||
self.labels = labels | ||
self.action = action | ||
let picker = MultilinePillPicker(labels: labels, action: action) | ||
super.init(AnyView(picker)) | ||
} | ||
|
||
@MainActor required dynamic init?(coder aDecoder: NSCoder) { | ||
fatalError("init(coder:) has not been implemented") | ||
} | ||
|
||
@MainActor required init(rootView: AnyView) { | ||
fatalError("init(rootView:) has not been implemented") | ||
} | ||
|
||
@MainActor public var isEnabled: Bool = true { | ||
didSet { | ||
updatePicker() | ||
} | ||
} | ||
@MainActor public var labels: [String] { | ||
didSet { | ||
updatePicker() | ||
} | ||
} | ||
@MainActor public var action: (@MainActor (Int) -> Void)? { | ||
didSet { | ||
updatePicker() | ||
} | ||
} | ||
|
||
private func updatePicker() { | ||
let picker = MultilinePillPicker(labels: labels, action: action) | ||
.disabled(!isEnabled) | ||
rootView = AnyView(picker) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
// | ||
// Copyright (c) Microsoft Corporation. All rights reserved. | ||
// Licensed under the MIT License. | ||
// | ||
|
||
import AppKit | ||
import SwiftUI | ||
|
||
/// Common wrapper for hosting and exposing SwiftUI components to AppKit-based clients. | ||
open class ControlHostingView: NSHostingView<AnyView> { | ||
/// Initializes and returns an instance of `ControlHostingContainer` that wraps `controlView`. | ||
/// | ||
/// Unfortunately this class can't use Swift generics, which are incompatible with Objective-C interop. Instead we have to wrap | ||
/// the control view in an `AnyView.` | ||
/// | ||
/// - Parameter controlView: An `AnyView`-wrapped component to host. | ||
/// - Parameter safeAreaRegions: Passthrough to the respective property on NSHostingView. | ||
/// Indicates which safe area regions the underlying hosting controller should add to its view. | ||
public init(_ controlView: AnyView, safeAreaRegions: SafeAreaRegions = .all) { | ||
super.init(rootView: controlView) | ||
if #available(macOS 13.3, *) { | ||
self.sizingOptions = [.intrinsicContentSize] | ||
self.safeAreaRegions = safeAreaRegions | ||
} | ||
|
||
layer?.backgroundColor = .clear | ||
translatesAutoresizingMaskIntoConstraints = false | ||
} | ||
|
||
@MainActor @preconcurrency required public init?(coder: NSCoder) { | ||
preconditionFailure("init(coder:) has not been implemented") | ||
} | ||
|
||
@MainActor @preconcurrency required public init(rootView: AnyView) { | ||
preconditionFailure("init(rootView:) has not been implemented") | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters