Skip to content

Commit 3d5f9c6

Browse files
authored
[macOS] Initial implementation of message bar for macOS (#2251)
* Initial implementation of message bar for macOS * Separate MessageBarStack from its hosting view * Add missing import
1 parent 9ff78ab commit 3d5f9c6

File tree

16 files changed

+668
-161
lines changed

16 files changed

+668
-161
lines changed

Demos/FluentUIDemo/Sources/Demo.swift

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,42 +9,36 @@ import SwiftUI
99
enum Demo: CaseIterable, Hashable {
1010
// Components
1111
case button
12+
case messageBar
1213
case shimmer
1314

1415
// Tokens
1516
case aliasColorTokens
1617

17-
var title: String {
18-
switch self {
19-
case .button:
20-
return "Button"
21-
case .shimmer:
22-
return "Shimmer"
23-
case .aliasColorTokens:
24-
return "Alias Color Tokens"
25-
}
26-
}
18+
var title: String { demoConfiguration.title }
2719

2820
/// Returns the `View` instance for the given demo.
29-
var view: any View {
30-
switch self {
31-
case .button:
32-
return ButtonDemoView()
33-
case .shimmer:
34-
return ShimmerDemoView()
35-
case .aliasColorTokens:
36-
return AliasColorTokensDemoView()
37-
}
38-
}
21+
var view: any View { demoConfiguration.view }
3922

4023
/// Only some demos are supported on visionOS.
41-
var supportsVisionOS: Bool {
24+
var supportsVisionOS: Bool { demoConfiguration.supportsVisionOS }
25+
26+
private struct DemoConfiguration {
27+
let title: String
28+
let view: any View
29+
let supportsVisionOS: Bool
30+
}
31+
32+
private var demoConfiguration: DemoConfiguration {
4233
switch self {
43-
case .button,
44-
.aliasColorTokens:
45-
return true
34+
case .button:
35+
return DemoConfiguration(title: "Button", view: ButtonDemoView(), supportsVisionOS: true)
36+
case .messageBar:
37+
return DemoConfiguration(title: "Message Bar", view: MessageBarDemoView(), supportsVisionOS: true)
4638
case .shimmer:
47-
return false
39+
return DemoConfiguration(title: "Shimmer", view: ShimmerDemoView(), supportsVisionOS: false)
40+
case .aliasColorTokens:
41+
return DemoConfiguration(title: "Alias Color Tokens", view: AliasColorTokensDemoView(), supportsVisionOS: true)
4842
}
4943
}
5044
}
@@ -91,6 +85,7 @@ enum DemoListSection: CaseIterable, Hashable {
9185
private struct Demos {
9286
static let fluent2: [Demo] = [
9387
.button,
88+
.messageBar,
9489
.shimmer
9590
]
9691

Demos/FluentUIDemo/Sources/DemoViews/AliasColorTokensDemoView.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,8 @@ extension FluentTheme.ColorToken {
182182
return "Foreground 2"
183183
case .foreground3:
184184
return "Foreground 3"
185+
case .foreground4:
186+
return "Foreground 4"
185187
case .foregroundDisabled1:
186188
return "Foreground Disabled 1"
187189
case .foregroundDisabled2:
@@ -226,6 +228,8 @@ extension FluentTheme.ColorToken {
226228
return "Background 4 Pressed"
227229
case .background4Selected:
228230
return "Background 4 Selected"
231+
case .background4Hover:
232+
return "Background 4 Hover"
229233
case .background5:
230234
return "Background 5"
231235
case .background5Pressed:
@@ -367,6 +371,7 @@ extension FluentTheme.ColorToken {
367371
.background4,
368372
.background4Pressed,
369373
.background4Selected,
374+
.background4Hover,
370375
.background5,
371376
.background5Pressed,
372377
.background5Selected,
@@ -395,6 +400,7 @@ extension FluentTheme.ColorToken {
395400
case .foreground1,
396401
.foreground2,
397402
.foreground3,
403+
.foreground4,
398404
.glassForeground1,
399405
.strokeFocus2,
400406
.strokeAccessible,
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
//
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
//
5+
6+
import SwiftUI
7+
import FluentUI
8+
9+
struct MessageBarDemoView: View {
10+
@Environment(\.fluentTheme) var fluentTheme
11+
12+
@State var alertText: String?
13+
@State var alertIsShown: Bool = false
14+
15+
var body: some View {
16+
VStack(alignment: .leading, spacing: .zero) {
17+
MessageBar(
18+
MessageBarConfiguration(
19+
title: "Descriptive Title",
20+
message: "Message providing information to the user with actionable insights.",
21+
hasCloseButton: true,
22+
onCloseCallback: {
23+
alertText = "Close button on the MessageBarView was pressed."
24+
alertIsShown = true
25+
}
26+
)
27+
)
28+
Spacer()
29+
}
30+
.frame(maxWidth: .infinity, maxHeight: .infinity)
31+
.background(fluentTheme.swiftUIColor(.background1))
32+
.alert("Button pressed!", isPresented: $alertIsShown) {
33+
Button("OK") {
34+
alertIsShown = false
35+
}
36+
} message: {
37+
Text(alertText ?? "")
38+
}
39+
}
40+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
//
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
//
5+
6+
import AppKit
7+
import FluentUI
8+
import SwiftUI
9+
10+
/// Test view controller for the MessageBar class
11+
class TestMessageBarController: NSViewController {
12+
override func viewDidLoad() {
13+
let stackingView = MessageBarStackHostingView()
14+
stackingView.translatesAutoresizingMaskIntoConstraints = false
15+
16+
view.addSubview(stackingView)
17+
18+
view.addConstraints([
19+
view.leadingAnchor.constraint(equalTo: stackingView.leadingAnchor),
20+
view.trailingAnchor.constraint(equalTo: stackingView.trailingAnchor),
21+
view.topAnchor.constraint(equalTo: stackingView.topAnchor),
22+
])
23+
24+
let configuration = MessageBarConfigurationObject()
25+
configuration.title = "Descriptive Title"
26+
configuration.message = "Message providing information to the user with actionable insights."
27+
configuration.hasCloseButton = true
28+
configuration.actionTitles = ["One", "Two"]
29+
configuration.onAction = { index in
30+
let alert = NSAlert()
31+
alert.messageText = "Action button \(index) on the MessageBarView was pressed."
32+
alert.runModal()
33+
}
34+
configuration.onClose = {
35+
let alert = NSAlert()
36+
alert.messageText = "Close button on the MessageBarView was pressed."
37+
alert.runModal()
38+
}
39+
stackingView.addBar(barID: 0, configuration: configuration)
40+
stackingView.showBar(barID: 0)
41+
stackingView.updateLayout()
42+
}
43+
}

Demos/FluentUIDemo_macOS/FluentUITestViewControllers/TestViewControllers.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ public let testViewControllers = [TestViewController(title: "Avatar View",
2727
type: TestGlassButtonController.self),
2828
TestViewController(title: "Link",
2929
type: TestLinkViewController.self),
30+
TestViewController(title: "Message Bar",
31+
type: TestMessageBarController.self),
3032
TestViewController(title: "Multiline Pill Picker",
3133
type: TestMultilinePillPickerViewController.self),
3234
TestViewController(title: "Notification Bar View",

Demos/FluentUIDemo_macOS/xcode/FluentUI.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
8F53680A2295F4C10098AC8F /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 8F5368082295F4C10098AC8F /* MainMenu.xib */; };
2525
9252C6222C62A8B3009C9272 /* FluentUI in Frameworks */ = {isa = PBXBuildFile; productRef = 9252C6212C62A8B3009C9272 /* FluentUI */; };
2626
925B6C0A2EB3CCD700B0898A /* TestGlassButtonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 925B6C092EB3CCD700B0898A /* TestGlassButtonController.swift */; };
27+
928582262F63E32C00DA8B74 /* TestMessageBarController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 928582252F63E32C00DA8B74 /* TestMessageBarController.swift */; };
2728
92AD71232D4062050089499E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 8F5368062295F4C10098AC8F /* Assets.xcassets */; };
2829
92AD71252D4062080089499E /* FluentUI in Frameworks */ = {isa = PBXBuildFile; productRef = 92AD71242D4062080089499E /* FluentUI */; };
2930
92AD71272D4064B50089499E /* TestMultilinePillPickerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 92AD71262D4064B00089499E /* TestMultilinePillPickerViewController.swift */; };
@@ -146,6 +147,7 @@
146147
8F931A6C22BD593300311764 /* FluentUI_unittest.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = FluentUI_unittest.xcconfig; sourceTree = "<group>"; };
147148
9252C61E2C62A881009C9272 /* fluentui-apple */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = "fluentui-apple"; path = ../../..; sourceTree = "<group>"; };
148149
925B6C092EB3CCD700B0898A /* TestGlassButtonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestGlassButtonController.swift; sourceTree = "<group>"; };
150+
928582252F63E32C00DA8B74 /* TestMessageBarController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMessageBarController.swift; sourceTree = "<group>"; };
149151
92AD711E2D4061340089499E /* FluentUIUnitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = FluentUIUnitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
150152
92AD71262D4064B00089499E /* TestMultilinePillPickerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestMultilinePillPickerViewController.swift; sourceTree = "<group>"; };
151153
9B4AEBAA2705206300B68020 /* TestFilledTemplateImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestFilledTemplateImageViewController.swift; sourceTree = "<group>"; };
@@ -312,6 +314,7 @@
312314
9B4AEBAA2705206300B68020 /* TestFilledTemplateImageViewController.swift */,
313315
925B6C092EB3CCD700B0898A /* TestGlassButtonController.swift */,
314316
E61C96D622987B3C0006561F /* TestLinkViewController.swift */,
317+
928582252F63E32C00DA8B74 /* TestMessageBarController.swift */,
315318
92AD71262D4064B00089499E /* TestMultilinePillPickerViewController.swift */,
316319
3A8CB0E12996CD6400B68FCF /* TestNotificationBarViewController.swift */,
317320
AC97EFE8247FAB1D00DADC99 /* TestSeparatorViewController.swift */,
@@ -646,6 +649,7 @@
646649
EC3AF6A726BDDD30009118F4 /* TestBadgeViewController.swift in Sources */,
647650
A257F81E2512DE45002CAA6E /* TestColorViewController.swift in Sources */,
648651
9B4AEBAB2705206300B68020 /* TestFilledTemplateImageViewController.swift in Sources */,
652+
928582262F63E32C00DA8B74 /* TestMessageBarController.swift in Sources */,
649653
E6A92D3024BEA8AC00562BCA /* TestLinkViewController.swift in Sources */,
650654
E6A92D2E24BEA8AC00562BCA /* TestAvatarViewController.swift in Sources */,
651655
E6A92D4F24BEAEEA00562BCA /* TestViewControllers.swift in Sources */,
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
//
2+
// Copyright (c) Microsoft Corporation. All rights reserved.
3+
// Licensed under the MIT License.
4+
//
5+
6+
#if canImport(AppKit)
7+
import AppKit
8+
import SwiftUI
9+
10+
public extension Font {
11+
static func fluent(_ fontInfo: FluentFontInfo, shouldScale: Bool = true) -> Font {
12+
// SwiftUI Font is missing some of the ease of construction available in NSFont.
13+
// So just leverage the logic there to create the equivalent SwiftUI font.
14+
let nsFont = NSFont.fluent(fontInfo, shouldScale: shouldScale)
15+
return Font(nsFont)
16+
}
17+
}
18+
19+
extension NSFont {
20+
@objc public static func fluent(_ fontInfo: FluentFontInfo, shouldScale: Bool = true) -> NSFont {
21+
let weight = nsWeight(fontInfo.weight)
22+
23+
if let name = fontInfo.name,
24+
let font = NSFont(name: name, size: fontInfo.size) {
25+
// Named font
26+
let unscaledFont = font.withWeight(weight)
27+
if shouldScale {
28+
return font.scale(nsTextStyle(fontInfo.textStyle))
29+
} else {
30+
return unscaledFont
31+
}
32+
} else {
33+
// System font
34+
if !shouldScale {
35+
return .systemFont(ofSize: fontInfo.size, weight: weight)
36+
}
37+
38+
let textStyle = nsTextStyle(fontInfo.textStyle)
39+
if fontInfo.matchesSystemSize {
40+
// System-recognized font size, let the OS scale it for us
41+
return NSFont.preferredFont(forTextStyle: textStyle).withWeight(weight)
42+
}
43+
44+
// Custom font size, we need to scale it ourselves
45+
return .systemFont(ofSize: fontInfo.size, weight: weight).scale(textStyle)
46+
}
47+
}
48+
49+
func scale(_ textStyle: NSFont.TextStyle) -> NSFont {
50+
let fontDescriptor = NSFontDescriptor.preferredFontDescriptor(forTextStyle: textStyle)
51+
let scaledFont = NSFont(descriptor: fontDescriptor, size: pointSize)!
52+
return scaledFont
53+
}
54+
55+
private func withWeight(_ weight: NSFont.Weight) -> NSFont {
56+
var attributes = fontDescriptor.fontAttributes
57+
var traits = (attributes[.traits] as? [NSFontDescriptor.TraitKey: Any]) ?? [:]
58+
59+
traits[.weight] = weight
60+
61+
// We need to remove `.name` since it may clash with the requested font weight, but
62+
// `.family` will ensure that e.g. Helvetica stays Helvetica.
63+
attributes[.name] = nil
64+
attributes[.traits] = traits
65+
attributes[.family] = familyName
66+
67+
let descriptor = NSFontDescriptor(fontAttributes: attributes)
68+
69+
return NSFont(descriptor: descriptor, size: pointSize)!
70+
}
71+
72+
private static func nsTextStyle(_ textStyle: Font.TextStyle) -> NSFont.TextStyle {
73+
switch textStyle {
74+
case .largeTitle:
75+
return .largeTitle
76+
case .title:
77+
return .title1
78+
case .title2:
79+
return .title2
80+
case .title3:
81+
return .title3
82+
case .headline:
83+
return .headline
84+
case .body:
85+
return .body
86+
case .callout:
87+
return .callout
88+
case .subheadline:
89+
return .subheadline
90+
case .footnote:
91+
return .footnote
92+
case .caption:
93+
return .caption1
94+
case .caption2:
95+
return .caption2
96+
default:
97+
// Font.TextStyle has `@unknown default` attribute, so we need a default.
98+
assertionFailure("Unknown Font.TextStyle found! Reverting to .body style.")
99+
return .body
100+
}
101+
}
102+
103+
private static func nsWeight(_ weight: Font.Weight) -> NSFont.Weight {
104+
switch weight {
105+
case .ultraLight:
106+
return .ultraLight
107+
case .thin:
108+
return .thin
109+
case .light:
110+
return .light
111+
case .regular:
112+
return .regular
113+
case .medium:
114+
return .medium
115+
case .semibold:
116+
return .semibold
117+
case .bold:
118+
return .bold
119+
case .heavy:
120+
return .heavy
121+
case .black:
122+
return .black
123+
default:
124+
// Font.Weight has `@unknown default` attribute, so we need a default.
125+
assertionFailure("Unknown Font.Weight found! Reverting to .regular weight.")
126+
return .regular
127+
}
128+
}
129+
}
130+
#endif // canImport(AppKit)

Sources/FluentUI_iOS/Core/Extensions/UIFont+Extensions.swift renamed to Sources/FluentUI_common/Core/Extensions/UIFont+Extensions.swift

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,7 @@
33
// Licensed under the MIT License.
44
//
55

6-
#if canImport(FluentUI_common)
7-
import FluentUI_common
8-
#endif
6+
#if canImport(UIKit)
97
import SwiftUI
108
import UIKit
119

@@ -136,3 +134,4 @@ extension UIFont {
136134
}
137135
}
138136
}
137+
#endif // canImport(UIKit)

Sources/FluentUI_common/Core/Theme/Extensions/macOS/FluentTheme+macOS.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,8 +276,8 @@ extension FluentTheme: PlatformThemeProviding {
276276
color = .init(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint10),
277277
dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint20))
278278
case .warningBackground1:
279-
color = .init(light: GlobalTokens.sharedSwiftUIColor(.yellow, .tint60),
280-
dark: GlobalTokens.sharedSwiftUIColor(.yellow, .shade40))
279+
color = .init(light: GlobalTokens.sharedSwiftUIColor(.yellow, .tint50),
280+
dark: GlobalTokens.sharedSwiftUIColor(.brass, .shade30))
281281
case .warningBackground2:
282282
color = .init(light: GlobalTokens.sharedSwiftUIColor(.yellow, .primary),
283283
dark: GlobalTokens.sharedSwiftUIColor(.yellow, .shade10))

0 commit comments

Comments
 (0)