From bd274ff9601b115a1998a1765e0ee41dc99bf5e5 Mon Sep 17 00:00:00 2001 From: Mike Schreiber Date: Mon, 21 Oct 2024 12:42:01 -0700 Subject: [PATCH] Create `FluentUI_common` module, part 1 (#2094) * Update common files to build in a separate module * Moving FluentTheme back to original location to help with diff * Make DynamicColor internal again * Remove `_DEPRECATED` * Fix whitespace issues --- .../NotificationViewDemoController.swift | 4 +- ...tificationViewDemoController_SwiftUI.swift | 4 +- .../Components/Badge Field/BadgeField.swift | 2 +- .../Components/Badge Field/BadgeView.swift | 2 +- .../BottomCommandingController.swift | 2 +- .../Bottom Sheet/BottomSheetController.swift | 2 +- .../Components/Button/Button.swift | 2 +- .../Calendar/Views/CalendarViewDayCell.swift | 2 +- .../Components/Card/CardView.swift | 2 +- .../Components/Command Bar/CommandBar.swift | 2 +- .../DateTimePickerViewComponentCell.swift | 2 +- .../Components/Drawer/DrawerController.swift | 2 +- .../Components/Label/BadgeLabel.swift | 2 +- .../FluentUI_iOS/Components/Label/Label.swift | 2 +- .../Components/Navigation/NavigationBar.swift | 2 +- .../Navigation/SearchBar/SearchBar.swift | 2 +- .../Navigation/Shy Header/ShyHeaderView.swift | 2 +- .../Navigation/Views/AvatarTitleView.swift | 2 +- .../Components/Other Cells/ActionsCell.swift | 2 +- .../Other Cells/ActivityIndicatorCell.swift | 2 +- .../Other Cells/CenteredLabelCell.swift | 2 +- .../Pill Button Bar/PillButton.swift | 2 +- .../Components/Popup Menu/PopupMenuItem.swift | 4 + .../ResizingHandleView.swift | 2 +- .../SegmentedControl/SegmentedControl.swift | 2 +- .../Components/Separator/Separator.swift | 2 +- .../Components/Shimmer/ShimmerView.swift | 2 +- .../Components/Tab Bar/SideTabBar.swift | 2 +- .../Components/Tab Bar/TabBarItemView.swift | 2 +- .../Components/Tab Bar/TabBarView.swift | 2 +- .../Components/Table View/TableViewCell.swift | 2 +- .../TableViewHeaderFooterView.swift | 2 +- .../TextField/FluentTextField.swift | 2 +- .../Components/Tooltip/Tooltip.swift | 2 +- .../TwoLineTitleView/TwoLineTitleView.swift | 2 +- ...amicColor.swift => Color+Extensions.swift} | 52 -- .../SwiftUI+ViewAnimation.swift | 0 .../SwiftUI+ViewModifiers.swift | 4 +- .../SwiftUI+ViewPresentation.swift | 0 .../Core/Extensions/UIFont+Extensions.swift | 135 +++++ .../Core/FluentTheme+Tokens.swift | 465 +++++++++--------- .../Core/Theme/FluentTheme+UIKit.swift | 121 ++++- .../Core/{ => Theme}/FluentTheme.swift | 95 +--- .../Core/Theme/Tokens/ControlTokenSet.swift | 77 +-- .../Core/Theme/Tokens/DynamicColor.swift | 44 ++ .../Core/Theme/Tokens/EmptyTokenSet.swift | 2 +- .../Core/Theme/Tokens/FontInfo.swift | 138 +----- .../Core/Theme/Tokens/ShadowInfo+UIKit.swift | 85 ++++ .../Core/Theme/Tokens/ShadowInfo.swift | 61 +-- .../Core/Theme/Tokens/TokenizedControl.swift | 6 - .../Theme/Tokens/TokenizedControlView.swift | 7 +- 51 files changed, 733 insertions(+), 635 deletions(-) rename Sources/FluentUI_iOS/Core/Extensions/{Color+DynamicColor.swift => Color+Extensions.swift} (50%) rename Sources/FluentUI_iOS/Core/{ => Extensions}/SwiftUI+ViewAnimation.swift (100%) rename Sources/FluentUI_iOS/Core/{ => Extensions}/SwiftUI+ViewModifiers.swift (98%) rename Sources/FluentUI_iOS/Core/{ => Extensions}/SwiftUI+ViewPresentation.swift (100%) create mode 100644 Sources/FluentUI_iOS/Core/Extensions/UIFont+Extensions.swift rename Sources/FluentUI_iOS/Core/{ => Theme}/FluentTheme.swift (63%) create mode 100644 Sources/FluentUI_iOS/Core/Theme/Tokens/DynamicColor.swift create mode 100644 Sources/FluentUI_iOS/Core/Theme/Tokens/ShadowInfo+UIKit.swift diff --git a/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/NotificationViewDemoController.swift b/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/NotificationViewDemoController.swift index 27f43375b6..f3916cd952 100644 --- a/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/NotificationViewDemoController.swift +++ b/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/NotificationViewDemoController.swift @@ -245,11 +245,11 @@ class NotificationViewDemoController: DemoController { return UIColor(light: GlobalTokens.sharedColor(.orange, .primary)) }, .shadow: .shadowInfo { - return ShadowInfo(keyColor: GlobalTokens.sharedColor(.hotPink, .primary), + return ShadowInfo(keyColor: GlobalTokens.sharedSwiftUIColor(.hotPink, .primary), keyBlur: 10.0, xKey: 10.0, yKey: 10.0, - ambientColor: GlobalTokens.sharedColor(.teal, .primary), + ambientColor: GlobalTokens.sharedSwiftUIColor(.teal, .primary), ambientBlur: 100.0, xAmbient: -10.0, yAmbient: -10.0) diff --git a/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/NotificationViewDemoController_SwiftUI.swift b/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/NotificationViewDemoController_SwiftUI.swift index 0f191de209..5ec53e3b6a 100644 --- a/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/NotificationViewDemoController_SwiftUI.swift +++ b/Demos/FluentUIDemo_iOS/FluentUI.Demo/Demos/NotificationViewDemoController_SwiftUI.swift @@ -307,11 +307,11 @@ struct NotificationDemoView: View { return GlobalTokens.sharedColor(.orange, .primary) }, .shadow: .shadowInfo { - return ShadowInfo(keyColor: GlobalTokens.sharedColor(.hotPink, .primary), + return ShadowInfo(keyColor: GlobalTokens.sharedSwiftUIColor(.hotPink, .primary), keyBlur: 10.0, xKey: 10.0, yKey: 10.0, - ambientColor: GlobalTokens.sharedColor(.teal, .primary), + ambientColor: GlobalTokens.sharedSwiftUIColor(.teal, .primary), ambientBlur: 100.0, xAmbient: -10.0, yAmbient: -10.0) diff --git a/Sources/FluentUI_iOS/Components/Badge Field/BadgeField.swift b/Sources/FluentUI_iOS/Components/Badge Field/BadgeField.swift index 4a822f22ae..05e422a86f 100644 --- a/Sources/FluentUI_iOS/Components/Badge Field/BadgeField.swift +++ b/Sources/FluentUI_iOS/Components/Badge Field/BadgeField.swift @@ -57,7 +57,7 @@ public protocol BadgeFieldDelegate: AnyObject { * voiceover and dynamic text sizing */ @objc(MSFBadgeField) -open class BadgeField: UIView, TokenizedControlInternal { +open class BadgeField: UIView, TokenizedControl { private struct Constants { static let emptyTextFieldString: String = "" static let dragAndDropMinimumPressDuration: TimeInterval = 0.2 diff --git a/Sources/FluentUI_iOS/Components/Badge Field/BadgeView.swift b/Sources/FluentUI_iOS/Components/Badge Field/BadgeView.swift index a4058e29d0..be0e0773f2 100644 --- a/Sources/FluentUI_iOS/Components/Badge Field/BadgeView.swift +++ b/Sources/FluentUI_iOS/Components/Badge Field/BadgeView.swift @@ -56,7 +56,7 @@ public protocol BadgeViewDelegate { `BadgeView` can be selected with a tap gesture and tapped again after entering a selected state for the purpose of displaying more details about the entity represented by the selected badge. */ @objc(MSFBadgeView) -open class BadgeView: UIView, TokenizedControlInternal { +open class BadgeView: UIView, TokenizedControl { @objc open var dataSource: BadgeViewDataSource? { didSet { reload() diff --git a/Sources/FluentUI_iOS/Components/Bottom Commanding/BottomCommandingController.swift b/Sources/FluentUI_iOS/Components/Bottom Commanding/BottomCommandingController.swift index e30935751e..86b2b60ff4 100644 --- a/Sources/FluentUI_iOS/Components/Bottom Commanding/BottomCommandingController.swift +++ b/Sources/FluentUI_iOS/Components/Bottom Commanding/BottomCommandingController.swift @@ -60,7 +60,7 @@ public protocol BottomCommandingControllerDelegate: AnyObject { /// Items from the `expandedListSections` are either presented in an expanded sheet or a popover, depending on the current style. /// @objc(MSFBottomCommandingController) -open class BottomCommandingController: UIViewController, TokenizedControlInternal { +open class BottomCommandingController: UIViewController, TokenizedControl { /// View controller that will be displayed below the bottom commanding UI. @objc public var contentViewController: UIViewController? { diff --git a/Sources/FluentUI_iOS/Components/Bottom Sheet/BottomSheetController.swift b/Sources/FluentUI_iOS/Components/Bottom Sheet/BottomSheetController.swift index 4a44a353cb..00a08e2c75 100644 --- a/Sources/FluentUI_iOS/Components/Bottom Sheet/BottomSheetController.swift +++ b/Sources/FluentUI_iOS/Components/Bottom Sheet/BottomSheetController.swift @@ -65,7 +65,7 @@ public protocol BottomSheetControllerDelegate: AnyObject { } @objc(MSFBottomSheetController) -public class BottomSheetController: UIViewController, Shadowable, TokenizedControlInternal { +public class BottomSheetController: UIViewController, Shadowable, TokenizedControl { /// Initializes the bottom sheet controller /// - Parameters: diff --git a/Sources/FluentUI_iOS/Components/Button/Button.swift b/Sources/FluentUI_iOS/Components/Button/Button.swift index fb84110133..5f8094d7ae 100644 --- a/Sources/FluentUI_iOS/Components/Button/Button.swift +++ b/Sources/FluentUI_iOS/Components/Button/Button.swift @@ -10,7 +10,7 @@ import UIKit /// By default, `titleLabel`'s `adjustsFontForContentSizeCategory` is set to true for non-floating buttons to automatically update its font when device's content size category changes @IBDesignable @objc(MSFButton) -open class Button: UIButton, Shadowable, TokenizedControlInternal { +open class Button: UIButton, Shadowable, TokenizedControl { @objc open var style: ButtonStyle = .outlineAccent { didSet { if style != oldValue { diff --git a/Sources/FluentUI_iOS/Components/Calendar/Views/CalendarViewDayCell.swift b/Sources/FluentUI_iOS/Components/Calendar/Views/CalendarViewDayCell.swift index 6621814609..788098241f 100644 --- a/Sources/FluentUI_iOS/Components/Calendar/Views/CalendarViewDayCell.swift +++ b/Sources/FluentUI_iOS/Components/Calendar/Views/CalendarViewDayCell.swift @@ -50,7 +50,7 @@ let calendarViewDayCellVisualStateTransitionDuration: TimeInterval = 0.3 // MARK: - CalendarViewDayCell -class CalendarViewDayCell: UICollectionViewCell, TokenizedControlInternal { +class CalendarViewDayCell: UICollectionViewCell, TokenizedControl { struct Constants { static let borderWidth: CGFloat = 0.5 static let dotDiameter: CGFloat = 6.0 diff --git a/Sources/FluentUI_iOS/Components/Card/CardView.swift b/Sources/FluentUI_iOS/Components/Card/CardView.swift index d39c8ad54b..95f2fe2f88 100644 --- a/Sources/FluentUI_iOS/Components/Card/CardView.swift +++ b/Sources/FluentUI_iOS/Components/Card/CardView.swift @@ -151,7 +151,7 @@ public enum CardSize: Int, CaseIterable { Conform to the `CardDelegate` in order to provide a handler for the card tap event */ @objc(MSFCardView) -open class CardView: UIView, Shadowable, TokenizedControlInternal { +open class CardView: UIView, Shadowable, TokenizedControl { /// Delegate to handle user interaction with the CardView @objc public weak var delegate: CardDelegate? diff --git a/Sources/FluentUI_iOS/Components/Command Bar/CommandBar.swift b/Sources/FluentUI_iOS/Components/Command Bar/CommandBar.swift index 6b598f7237..7e3cc3f449 100644 --- a/Sources/FluentUI_iOS/Components/Command Bar/CommandBar.swift +++ b/Sources/FluentUI_iOS/Components/Command Bar/CommandBar.swift @@ -18,7 +18,7 @@ public protocol CommandBarDelegate: AnyObject { Provide `itemGroups` in `init` to set the buttons in the CommandBar. Optional `leadingItemGroups` and `trailingItemGroups` add buttons in leading and trailing positions. Each `CommandBarItem` will be represented as a button. */ @objc(MSFCommandBar) -public class CommandBar: UIView, TokenizedControlInternal { +public class CommandBar: UIView, TokenizedControl { // Hierarchy: // // isScrollable = true diff --git a/Sources/FluentUI_iOS/Components/Date Time Pickers/Date Time Picker/Views/DateTimePickerViewComponentCell.swift b/Sources/FluentUI_iOS/Components/Date Time Pickers/Date Time Picker/Views/DateTimePickerViewComponentCell.swift index f4f030b750..db6625623c 100644 --- a/Sources/FluentUI_iOS/Components/Date Time Pickers/Date Time Picker/Views/DateTimePickerViewComponentCell.swift +++ b/Sources/FluentUI_iOS/Components/Date Time Pickers/Date Time Picker/Views/DateTimePickerViewComponentCell.swift @@ -8,7 +8,7 @@ import UIKit // MARK: - DateTimePickerViewComponentCell /// TableViewCell representing the cell of component view (should be used only by DateTimePickerViewComponent and not instantiated on its own) -class DateTimePickerViewComponentCell: UITableViewCell, TokenizedControlInternal { +class DateTimePickerViewComponentCell: UITableViewCell, TokenizedControl { private struct Constants { static let baseHeight: CGFloat = 45 static let verticalPadding: CGFloat = 12 diff --git a/Sources/FluentUI_iOS/Components/Drawer/DrawerController.swift b/Sources/FluentUI_iOS/Components/Drawer/DrawerController.swift index 3d789a2b45..fa2c008358 100644 --- a/Sources/FluentUI_iOS/Components/Drawer/DrawerController.swift +++ b/Sources/FluentUI_iOS/Components/Drawer/DrawerController.swift @@ -97,7 +97,7 @@ public protocol DrawerControllerDelegate: AnyObject { */ @objc(MSFDrawerController) -open class DrawerController: UIViewController, TokenizedControlInternal { +open class DrawerController: UIViewController, TokenizedControl { /// DrawerController colors with obj-c support @objc public static func drawerBackgroundColor(fluentTheme: FluentTheme?) -> UIColor { let theme = fluentTheme ?? .shared diff --git a/Sources/FluentUI_iOS/Components/Label/BadgeLabel.swift b/Sources/FluentUI_iOS/Components/Label/BadgeLabel.swift index da14eea4fd..f556f6110a 100644 --- a/Sources/FluentUI_iOS/Components/Label/BadgeLabel.swift +++ b/Sources/FluentUI_iOS/Components/Label/BadgeLabel.swift @@ -7,7 +7,7 @@ import UIKit // MARK: BadgeLabel -class BadgeLabel: UILabel, TokenizedControlInternal { +class BadgeLabel: UILabel, TokenizedControl { var style: BadgeLabelStyle = .system { didSet { updateColors() diff --git a/Sources/FluentUI_iOS/Components/Label/Label.swift b/Sources/FluentUI_iOS/Components/Label/Label.swift index 1d8f3f5153..911091c12a 100644 --- a/Sources/FluentUI_iOS/Components/Label/Label.swift +++ b/Sources/FluentUI_iOS/Components/Label/Label.swift @@ -9,7 +9,7 @@ import UIKit /// By default, `adjustsFontForContentSizeCategory` is set to true to automatically update its font when device's content size category changes @objc(MSFLabel) -open class Label: UILabel, TokenizedControlInternal { +open class Label: UILabel, TokenizedControl { private static let defaultColorForTheme: (FluentTheme) -> UIColor = TextColorStyle.regular.uiColor @objc open var colorStyle: TextColorStyle { diff --git a/Sources/FluentUI_iOS/Components/Navigation/NavigationBar.swift b/Sources/FluentUI_iOS/Components/Navigation/NavigationBar.swift index ae978fd1f7..34af15a829 100644 --- a/Sources/FluentUI_iOS/Components/Navigation/NavigationBar.swift +++ b/Sources/FluentUI_iOS/Components/Navigation/NavigationBar.swift @@ -100,7 +100,7 @@ protocol NavigationBarBackButtonDelegate { /// Contains the MSNavigationTitleView class and handles passing animatable progress through /// Custom UI can be hidden if desired @objc(MSFNavigationBar) -open class NavigationBar: UINavigationBar, TokenizedControlInternal, TwoLineTitleViewDelegate { +open class NavigationBar: UINavigationBar, TokenizedControl, TwoLineTitleViewDelegate { /// If the style is `.custom`, UINavigationItem's `navigationBarColor` is used for all the subviews' backgroundColor @objc(MSFNavigationBarStyle) public enum Style: Int { diff --git a/Sources/FluentUI_iOS/Components/Navigation/SearchBar/SearchBar.swift b/Sources/FluentUI_iOS/Components/Navigation/SearchBar/SearchBar.swift index a2a72a6b7c..47efdc4236 100644 --- a/Sources/FluentUI_iOS/Components/Navigation/SearchBar/SearchBar.swift +++ b/Sources/FluentUI_iOS/Components/Navigation/SearchBar/SearchBar.swift @@ -22,7 +22,7 @@ public protocol SearchBarDelegate: AnyObject { /// Drop-in replacement for UISearchBar that allows for more customization @objc(MSFSearchBar) -open class SearchBar: UIView, TokenizedControlInternal { +open class SearchBar: UIView, TokenizedControl { @objc open var hidesNavigationBarDuringSearch: Bool = true { didSet { if oldValue != hidesNavigationBarDuringSearch && isActive { diff --git a/Sources/FluentUI_iOS/Components/Navigation/Shy Header/ShyHeaderView.swift b/Sources/FluentUI_iOS/Components/Navigation/Shy Header/ShyHeaderView.swift index 55e9fcebe4..d9f2e505d1 100644 --- a/Sources/FluentUI_iOS/Components/Navigation/Shy Header/ShyHeaderView.swift +++ b/Sources/FluentUI_iOS/Components/Navigation/Shy Header/ShyHeaderView.swift @@ -11,7 +11,7 @@ import UIKit /// Used to contain an accessory provided by the VC contained by the NavigatableShyContainerVC /// This class in itself is fairly straightforward, defining a height and a containment layout /// The animation around showing/hiding this view progressively is handled by its superview/superVC, an instance of ShyHeaderController -class ShyHeaderView: UIView, TokenizedControlInternal { +class ShyHeaderView: UIView, TokenizedControl { typealias TokenSetKeyType = EmptyTokenSet.Tokens public var tokenSet: EmptyTokenSet = .init() diff --git a/Sources/FluentUI_iOS/Components/Navigation/Views/AvatarTitleView.swift b/Sources/FluentUI_iOS/Components/Navigation/Views/AvatarTitleView.swift index 25de74b2ec..5654d3019c 100644 --- a/Sources/FluentUI_iOS/Components/Navigation/Views/AvatarTitleView.swift +++ b/Sources/FluentUI_iOS/Components/Navigation/Views/AvatarTitleView.swift @@ -8,7 +8,7 @@ import UIKit // MARK: AvatarTitleView /// A helper view used by `NavigationBar` capable of displaying a large title and an avatar. -class AvatarTitleView: UIView, TokenizedControlInternal, TwoLineTitleViewDelegate { +class AvatarTitleView: UIView, TokenizedControl, TwoLineTitleViewDelegate { enum Style: Int { case primary case system diff --git a/Sources/FluentUI_iOS/Components/Other Cells/ActionsCell.swift b/Sources/FluentUI_iOS/Components/Other Cells/ActionsCell.swift index 8d18aed6ca..0f9c975dd8 100644 --- a/Sources/FluentUI_iOS/Components/Other Cells/ActionsCell.swift +++ b/Sources/FluentUI_iOS/Components/Other Cells/ActionsCell.swift @@ -15,7 +15,7 @@ import UIKit `topSeparatorType` and `bottomSeparatorType` can be used to show custom horizontal separators. Make sure to remove the `UITableViewCell` built-in separator by setting `separatorStyle = .none` on your table view. */ @objc(MSFActionsCell) -open class ActionsCell: UITableViewCell, TokenizedControlInternal { +open class ActionsCell: UITableViewCell, TokenizedControl { @objc(MSFActionsCellActionType) public enum ActionType: Int { case regular diff --git a/Sources/FluentUI_iOS/Components/Other Cells/ActivityIndicatorCell.swift b/Sources/FluentUI_iOS/Components/Other Cells/ActivityIndicatorCell.swift index 046131e59d..c396b65d7e 100644 --- a/Sources/FluentUI_iOS/Components/Other Cells/ActivityIndicatorCell.swift +++ b/Sources/FluentUI_iOS/Components/Other Cells/ActivityIndicatorCell.swift @@ -8,7 +8,7 @@ import UIKit // MARK: ActivityIndicatorCell @objc(MSFActivityIndicatorCell) -open class ActivityIndicatorCell: UITableViewCell, TokenizedControlInternal { +open class ActivityIndicatorCell: UITableViewCell, TokenizedControl { public static let identifier: String = "ActivityIndicatorCell" @objc public var backgroundStyleType: TableViewCellBackgroundStyleType = .plain { diff --git a/Sources/FluentUI_iOS/Components/Other Cells/CenteredLabelCell.swift b/Sources/FluentUI_iOS/Components/Other Cells/CenteredLabelCell.swift index 1ae7cf6ce4..1034f450fc 100644 --- a/Sources/FluentUI_iOS/Components/Other Cells/CenteredLabelCell.swift +++ b/Sources/FluentUI_iOS/Components/Other Cells/CenteredLabelCell.swift @@ -8,7 +8,7 @@ import UIKit // MARK: CenteredLabelCell @objc(MSFCenteredLabelCell) -open class CenteredLabelCell: UITableViewCell, TokenizedControlInternal { +open class CenteredLabelCell: UITableViewCell, TokenizedControl { public static let identifier: String = "CenteredLabelCell" public typealias TokenSetKeyType = TableViewCellTokenSet.Tokens diff --git a/Sources/FluentUI_iOS/Components/Pill Button Bar/PillButton.swift b/Sources/FluentUI_iOS/Components/Pill Button Bar/PillButton.swift index afbd8dc672..8cd300fb83 100644 --- a/Sources/FluentUI_iOS/Components/Pill Button Bar/PillButton.swift +++ b/Sources/FluentUI_iOS/Components/Pill Button Bar/PillButton.swift @@ -9,7 +9,7 @@ import UIKit /// A `PillButton` is a button in the shape of a pill that can have two states: on (Selected) and off (not selected) @objc(MSFPillButton) -open class PillButton: UIButton, TokenizedControlInternal { +open class PillButton: UIButton, TokenizedControl { open override func didUpdateFocus(in context: UIFocusUpdateContext, with coordinator: UIFocusAnimationCoordinator) { guard self == context.nextFocusedView || self == context.previouslyFocusedView else { diff --git a/Sources/FluentUI_iOS/Components/Popup Menu/PopupMenuItem.swift b/Sources/FluentUI_iOS/Components/Popup Menu/PopupMenuItem.swift index d4b36f48f6..2d169b3c0d 100644 --- a/Sources/FluentUI_iOS/Components/Popup Menu/PopupMenuItem.swift +++ b/Sources/FluentUI_iOS/Components/Popup Menu/PopupMenuItem.swift @@ -101,6 +101,10 @@ open class PopupMenuItem: NSObject, PopupMenuTemplateItem, FluentThemeable { self.init(image: image, selectedImage: selectedImage, title: title, subtitle: subtitle, isEnabled: isEnabled, isSelected: isSelected, executes: executionMode, onSelected: onSelected, isAccessoryCheckmarkVisible: isAccessoryCheckmarkVisible) } + public func isApplicableThemeChange(_ notification: Notification) -> Bool { + return true + } + lazy var tokenSet: PopupMenuItemTokenSet = { PopupMenuItemTokenSet(customViewSize: { self.image != nil ? .small : .zero }) }() diff --git a/Sources/FluentUI_iOS/Components/ResizingHandleView/ResizingHandleView.swift b/Sources/FluentUI_iOS/Components/ResizingHandleView/ResizingHandleView.swift index 323a53c02f..d48b7f4d94 100644 --- a/Sources/FluentUI_iOS/Components/ResizingHandleView/ResizingHandleView.swift +++ b/Sources/FluentUI_iOS/Components/ResizingHandleView/ResizingHandleView.swift @@ -8,7 +8,7 @@ import UIKit // MARK: - ResizingHandleView @objc(MSFResizingHandleView) -open class ResizingHandleView: UIView, TokenizedControlInternal { +open class ResizingHandleView: UIView, TokenizedControl { @objc public static let height: CGFloat = 20 private lazy var markLayer: CALayer = { diff --git a/Sources/FluentUI_iOS/Components/SegmentedControl/SegmentedControl.swift b/Sources/FluentUI_iOS/Components/SegmentedControl/SegmentedControl.swift index 5a49e687fb..0a0b939c62 100644 --- a/Sources/FluentUI_iOS/Components/SegmentedControl/SegmentedControl.swift +++ b/Sources/FluentUI_iOS/Components/SegmentedControl/SegmentedControl.swift @@ -7,7 +7,7 @@ import UIKit // MARK: SegmentedControl /// A styled segmented control that should be used instead of UISegmentedControl. It is designed to flex the button width proportionally to the control's width. @objc(MSFSegmentedControl) -open class SegmentedControl: UIView, TokenizedControlInternal { +open class SegmentedControl: UIView, TokenizedControl { private struct Constants { static let iPadMinimumWidth: CGFloat = 375 } diff --git a/Sources/FluentUI_iOS/Components/Separator/Separator.swift b/Sources/FluentUI_iOS/Components/Separator/Separator.swift index 72120db724..4ffa37c8c8 100644 --- a/Sources/FluentUI_iOS/Components/Separator/Separator.swift +++ b/Sources/FluentUI_iOS/Components/Separator/Separator.swift @@ -16,7 +16,7 @@ public enum SeparatorOrientation: Int { // MARK: - Separator @objc(MSFSeparator) -open class Separator: UIView, TokenizedControlInternal { +open class Separator: UIView, TokenizedControl { public typealias TokenSetKeyType = SeparatorTokenSet.Tokens lazy public var tokenSet: SeparatorTokenSet = .init() diff --git a/Sources/FluentUI_iOS/Components/Shimmer/ShimmerView.swift b/Sources/FluentUI_iOS/Components/Shimmer/ShimmerView.swift index 307f0da51e..3163bcbf28 100644 --- a/Sources/FluentUI_iOS/Components/Shimmer/ShimmerView.swift +++ b/Sources/FluentUI_iOS/Components/Shimmer/ShimmerView.swift @@ -7,7 +7,7 @@ import UIKit /// View that converts the subviews of a container view into a loading state with the "shimmering" effect. @objc(MSFShimmerView) -open class ShimmerView: UIView, TokenizedControlInternal { +open class ShimmerView: UIView, TokenizedControl { /// Optional synchronizer to sync multiple shimmer views. @objc open weak var animationSynchronizer: AnimationSynchronizerProtocol? diff --git a/Sources/FluentUI_iOS/Components/Tab Bar/SideTabBar.swift b/Sources/FluentUI_iOS/Components/Tab Bar/SideTabBar.swift index 9c63e8ca80..4d3408fcde 100644 --- a/Sources/FluentUI_iOS/Components/Tab Bar/SideTabBar.swift +++ b/Sources/FluentUI_iOS/Components/Tab Bar/SideTabBar.swift @@ -23,7 +23,7 @@ public protocol SideTabBarDelegate { /// View for a vertical side tab bar that can be used for app navigation. /// Optimized for horizontal regular + vertical regular size class configuration. Prefer using TabBarView for other size class configurations. @objc(MSFSideTabBar) -open class SideTabBar: UIView, TokenizedControlInternal { +open class SideTabBar: UIView, TokenizedControl { /// Delegate to handle user interactions in the side tab bar. @objc public weak var delegate: SideTabBarDelegate? { didSet { diff --git a/Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift b/Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift index b66e1ddb9a..7a28fbffcd 100644 --- a/Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift +++ b/Sources/FluentUI_iOS/Components/Tab Bar/TabBarItemView.swift @@ -5,7 +5,7 @@ import UIKit -class TabBarItemView: UIControl, TokenizedControlInternal { +class TabBarItemView: UIControl, TokenizedControl { let item: TabBarItem typealias TokenSetKeyType = TabBarItemTokenSet.Tokens diff --git a/Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift b/Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift index 4972ffe706..6688e2eb06 100644 --- a/Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift +++ b/Sources/FluentUI_iOS/Components/Tab Bar/TabBarView.swift @@ -20,7 +20,7 @@ public protocol TabBarViewDelegate { /// Set up `items` array to determine the order of `TabBarItems` to show. /// Use `selectedItem` property to change the selected tab bar item. @objc(MSFTabBarView) -open class TabBarView: UIView, TokenizedControlInternal { +open class TabBarView: UIView, TokenizedControl { /// List of TabBarItems in the TabBarView. Order of the array is the order of the subviews. @objc open var items: [TabBarItem] = [] { willSet { diff --git a/Sources/FluentUI_iOS/Components/Table View/TableViewCell.swift b/Sources/FluentUI_iOS/Components/Table View/TableViewCell.swift index cd34e90ef7..ff03d47cee 100644 --- a/Sources/FluentUI_iOS/Components/Table View/TableViewCell.swift +++ b/Sources/FluentUI_iOS/Components/Table View/TableViewCell.swift @@ -34,7 +34,7 @@ Specify `accessoryType` on setup to show either a disclosure indicator or a `det NOTE: This cell implements its own custom separator. Make sure to remove the UITableViewCell built-in separator by setting `separatorStyle = .none` on your table view. To remove the cell's custom separator set `bottomSeparatorType` to `.none`. */ @objc(MSFTableViewCell) -open class TableViewCell: UITableViewCell, TokenizedControlInternal { +open class TableViewCell: UITableViewCell, TokenizedControl { @objc(MSFTableViewCellSeparatorType) public enum SeparatorType: Int { case none diff --git a/Sources/FluentUI_iOS/Components/Table View/TableViewHeaderFooterView.swift b/Sources/FluentUI_iOS/Components/Table View/TableViewHeaderFooterView.swift index be240c7cf3..209242a283 100644 --- a/Sources/FluentUI_iOS/Components/Table View/TableViewHeaderFooterView.swift +++ b/Sources/FluentUI_iOS/Components/Table View/TableViewHeaderFooterView.swift @@ -44,7 +44,7 @@ public protocol TableViewHeaderFooterViewDelegate: AnyObject { /// The optional accessory button should only be used with `default` style headers with the `title` as a single line of text. /// Use `titleNumberOfLines` to configure the number of lines for the `title`. Headers generally use the default number of lines of 1 while footers may use a multiple number of lines. @objc(MSFTableViewHeaderFooterView) -open class TableViewHeaderFooterView: UITableViewHeaderFooterView, TokenizedControlInternal { +open class TableViewHeaderFooterView: UITableViewHeaderFooterView, TokenizedControl { @objc public static var identifier: String { return String(describing: self) } /// The height of the view based on the height of its content. diff --git a/Sources/FluentUI_iOS/Components/TextField/FluentTextField.swift b/Sources/FluentUI_iOS/Components/TextField/FluentTextField.swift index e19314ec29..580ece3ee2 100644 --- a/Sources/FluentUI_iOS/Components/TextField/FluentTextField.swift +++ b/Sources/FluentUI_iOS/Components/TextField/FluentTextField.swift @@ -6,7 +6,7 @@ import UIKit @objc(MSFTextField) -public final class FluentTextField: UIView, UITextFieldDelegate, TokenizedControlInternal { +public final class FluentTextField: UIView, UITextFieldDelegate, TokenizedControl { public override func willMove(toWindow newWindow: UIWindow?) { super.willMove(toWindow: newWindow) guard let newWindow else { diff --git a/Sources/FluentUI_iOS/Components/Tooltip/Tooltip.swift b/Sources/FluentUI_iOS/Components/Tooltip/Tooltip.swift index 61396e0ccb..b8c2d3cadf 100644 --- a/Sources/FluentUI_iOS/Components/Tooltip/Tooltip.swift +++ b/Sources/FluentUI_iOS/Components/Tooltip/Tooltip.swift @@ -17,7 +17,7 @@ import UIKit // |--|--layer (ambient and key shadows added as sublayers) /// A styled tooltip that is presented anchored to a view. @objc(MSFTooltip) -open class Tooltip: NSObject, TokenizedControlInternal { +open class Tooltip: NSObject, TokenizedControl { /// Displays a tooltip based on the current settings, pointing to the supplied anchorView. /// If another tooltip view is already showing, it will be dismissed and the new tooltip will be shown. diff --git a/Sources/FluentUI_iOS/Components/TwoLineTitleView/TwoLineTitleView.swift b/Sources/FluentUI_iOS/Components/TwoLineTitleView/TwoLineTitleView.swift index ac096e8b7f..0dc4ff424d 100644 --- a/Sources/FluentUI_iOS/Components/TwoLineTitleView/TwoLineTitleView.swift +++ b/Sources/FluentUI_iOS/Components/TwoLineTitleView/TwoLineTitleView.swift @@ -17,7 +17,7 @@ public protocol TwoLineTitleViewDelegate: AnyObject { // MARK: - TwoLineTitleView @objc(MSFTwoLineTitleView) -open class TwoLineTitleView: UIView, TokenizedControlInternal { +open class TwoLineTitleView: UIView, TokenizedControl { @objc(MSFTwoLineTitleViewStyle) public enum Style: Int { case primary diff --git a/Sources/FluentUI_iOS/Core/Extensions/Color+DynamicColor.swift b/Sources/FluentUI_iOS/Core/Extensions/Color+Extensions.swift similarity index 50% rename from Sources/FluentUI_iOS/Core/Extensions/Color+DynamicColor.swift rename to Sources/FluentUI_iOS/Core/Extensions/Color+Extensions.swift index a30c8afd1d..f585e6718f 100644 --- a/Sources/FluentUI_iOS/Core/Extensions/Color+DynamicColor.swift +++ b/Sources/FluentUI_iOS/Core/Extensions/Color+Extensions.swift @@ -39,56 +39,4 @@ extension Color { self.init(uiColor: UIColor(dynamicColor: dynamicColor)) } } - - init(dynamicColor: DynamicColor) { - if #available(iOS 17, *) { - self.init(dynamicColor) - } else { - self.init(uiColor: UIColor(dynamicColor: dynamicColor)) - } - } -} - -/// A container that stores a dynamic set of `Color` values. -struct DynamicColor: Hashable { - - /// Creates a custom `ShapeStyle` that stores a dynamic set of `Color` values. - /// - /// - Parameter light: The default `Color` for a light context. Required. - /// - Parameter dark: The override `Color` for a dark context. Optional. - /// - Parameter darkElevated: The override `Color` for a dark elevated context. Optional. - init(light: Color, - dark: Color? = nil, - darkElevated: Color? = nil) { - self.light = light - self.dark = dark - self.darkElevated = darkElevated - } - - init(uiColor: UIColor) { - self.init(light: Color(uiColor.light), - dark: Color(uiColor.dark), - darkElevated: Color(uiColor.darkElevated)) - } - - let light: Color - let dark: Color? - let darkElevated: Color? -} - -@available(iOS 17, *) -extension DynamicColor: ShapeStyle { - /// Evaluate to a resolved `Color` (in the form of a `ShapeStyle`) given the current `environment`. - func resolve(in environment: EnvironmentValues) -> Color.Resolved { - if environment.colorScheme == .dark { - if environment.isPresented, let darkElevated = darkElevated { - return darkElevated.resolve(in: environment) - } else if let dark = dark { - return dark.resolve(in: environment) - } - } - - // default - return light.resolve(in: environment) - } } diff --git a/Sources/FluentUI_iOS/Core/SwiftUI+ViewAnimation.swift b/Sources/FluentUI_iOS/Core/Extensions/SwiftUI+ViewAnimation.swift similarity index 100% rename from Sources/FluentUI_iOS/Core/SwiftUI+ViewAnimation.swift rename to Sources/FluentUI_iOS/Core/Extensions/SwiftUI+ViewAnimation.swift diff --git a/Sources/FluentUI_iOS/Core/SwiftUI+ViewModifiers.swift b/Sources/FluentUI_iOS/Core/Extensions/SwiftUI+ViewModifiers.swift similarity index 98% rename from Sources/FluentUI_iOS/Core/SwiftUI+ViewModifiers.swift rename to Sources/FluentUI_iOS/Core/Extensions/SwiftUI+ViewModifiers.swift index 793d813138..5093a8ea42 100644 --- a/Sources/FluentUI_iOS/Core/SwiftUI+ViewModifiers.swift +++ b/Sources/FluentUI_iOS/Core/Extensions/SwiftUI+ViewModifiers.swift @@ -138,11 +138,11 @@ struct ShadowModifier: ViewModifier { func body(content: Content) -> some View { content - .shadow(color: Color(shadowInfo.ambientColor), + .shadow(color: shadowInfo.ambientColor, radius: shadowInfo.ambientBlur, x: shadowInfo.xAmbient, y: shadowInfo.yAmbient) - .shadow(color: Color(shadowInfo.keyColor), + .shadow(color: shadowInfo.keyColor, radius: shadowInfo.keyBlur, x: shadowInfo.xKey, y: shadowInfo.yKey) diff --git a/Sources/FluentUI_iOS/Core/SwiftUI+ViewPresentation.swift b/Sources/FluentUI_iOS/Core/Extensions/SwiftUI+ViewPresentation.swift similarity index 100% rename from Sources/FluentUI_iOS/Core/SwiftUI+ViewPresentation.swift rename to Sources/FluentUI_iOS/Core/Extensions/SwiftUI+ViewPresentation.swift diff --git a/Sources/FluentUI_iOS/Core/Extensions/UIFont+Extensions.swift b/Sources/FluentUI_iOS/Core/Extensions/UIFont+Extensions.swift new file mode 100644 index 0000000000..36c5d45f7f --- /dev/null +++ b/Sources/FluentUI_iOS/Core/Extensions/UIFont+Extensions.swift @@ -0,0 +1,135 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +import SwiftUI +import UIKit + +public extension Font { + static func fluent(_ fontInfo: FontInfo, shouldScale: Bool = true) -> Font { + // SwiftUI Font is missing some of the ease of construction available in UIFont. + // So just leverage the logic there to create the equivalent SwiftUI font. + let uiFont = UIFont.fluent(fontInfo, shouldScale: shouldScale) + return Font(uiFont) + } +} + +extension UIFont { + @objc public static func fluent(_ fontInfo: FontInfo, shouldScale: Bool = true) -> UIFont { + return fluent(fontInfo, shouldScale: shouldScale, contentSizeCategory: nil) + } + + @objc public static func fluent(_ fontInfo: FontInfo, shouldScale: Bool = true, contentSizeCategory: UIContentSizeCategory?) -> UIFont { + let traitCollection: UITraitCollection? + if let contentSizeCategory = contentSizeCategory { + traitCollection = .init(preferredContentSizeCategory: contentSizeCategory) + } else { + traitCollection = nil + } + + let weight = uiWeight(fontInfo.weight) + + if let name = fontInfo.name, + let font = UIFont(name: name, size: fontInfo.size) { + // Named font + let unscaledFont = font.withWeight(weight) + if shouldScale { + let fontMetrics = UIFontMetrics(forTextStyle: uiTextStyle(fontInfo.textStyle)) + return fontMetrics.scaledFont(for: unscaledFont, compatibleWith: traitCollection) + } else { + return unscaledFont + } + } else { + // System font + if !shouldScale { + return .systemFont(ofSize: fontInfo.size, weight: weight) + } + + let textStyle = uiTextStyle(fontInfo.textStyle) + if fontInfo.matchesSystemSize { + // System-recognized font size, let the OS scale it for us + return UIFont.preferredFont(forTextStyle: textStyle, compatibleWith: traitCollection).withWeight(weight) + } + + // Custom font size, we need to scale it ourselves + let fontMetrics = UIFontMetrics(forTextStyle: textStyle) + return fontMetrics.scaledFont(for: .systemFont(ofSize: fontInfo.size, weight: weight), compatibleWith: traitCollection) + } + } + + private func withWeight(_ weight: UIFont.Weight) -> UIFont { + var attributes = fontDescriptor.fontAttributes + var traits = (attributes[.traits] as? [UIFontDescriptor.TraitKey: Any]) ?? [:] + + traits[.weight] = weight + + // We need to remove `.name` since it may clash with the requested font weight, but + // `.family` will ensure that e.g. Helvetica stays Helvetica. + attributes[.name] = nil + attributes[.traits] = traits + attributes[.family] = familyName + + let descriptor = UIFontDescriptor(fontAttributes: attributes) + + return UIFont(descriptor: descriptor, size: pointSize) + } + + private static func uiTextStyle(_ textStyle: Font.TextStyle) -> UIFont.TextStyle { + switch textStyle { + case .largeTitle: + return .largeTitle + case .title: + return .title1 + case .title2: + return .title2 + case .title3: + return .title3 + case .headline: + return .headline + case .body: + return .body + case .callout: + return .callout + case .subheadline: + return .subheadline + case .footnote: + return .footnote + case .caption: + return .caption1 + case .caption2: + return .caption2 + default: + // Font.TextStyle has `@unknown default` attribute, so we need a default. + assertionFailure("Unknown Font.TextStyle found! Reverting to .body style.") + return .body + } + } + + private static func uiWeight(_ weight: Font.Weight) -> UIFont.Weight { + switch weight { + case .ultraLight: + return .ultraLight + case .thin: + return .thin + case .light: + return .light + case .regular: + return .regular + case .medium: + return .medium + case .semibold: + return .semibold + case .bold: + return .bold + case .heavy: + return .heavy + case .black: + return .black + default: + // Font.Weight has `@unknown default` attribute, so we need a default. + assertionFailure("Unknown Font.Weight found! Reverting to .regular weight.") + return .regular + } + } +} diff --git a/Sources/FluentUI_iOS/Core/FluentTheme+Tokens.swift b/Sources/FluentUI_iOS/Core/FluentTheme+Tokens.swift index 1efa587f69..0706440344 100644 --- a/Sources/FluentUI_iOS/Core/FluentTheme+Tokens.swift +++ b/Sources/FluentUI_iOS/Core/FluentTheme+Tokens.swift @@ -150,7 +150,7 @@ public extension FluentTheme { /// - Parameter token: The `ColorsTokens` value to be retrieved. /// - Returns: A `Color` for the given token. func swiftUIColor(_ token: ColorToken) -> Color { - return Color(dynamicColor: colorTokenSet[token]) + return colorTokenSet[token] } /// Returns the shadow value for the given token. @@ -165,27 +165,10 @@ public extension FluentTheme { /// Returns the font value for the given token. /// /// - Parameter token: The `TypographyTokens` value to be retrieved. - /// - Parameter adjustsForContentSizeCategory: If true, the resulting font will change size according to Dynamic Type specifications. /// - Returns: A `UIFont` for the given token. - @objc(typographyForToken:adjustsForContentSizeCategory:) - func typography(_ token: TypographyToken, adjustsForContentSizeCategory: Bool = true) -> UIFont { - return UIFont.fluent(typographyTokenSet[token], - shouldScale: adjustsForContentSizeCategory) - } - - /// Returns the font value for the given token. - /// - /// - Parameter token: The `TypographyTokens` value to be retrieved. - /// - Parameter adjustsForContentSizeCategory: If true, the resulting font will change size according to Dynamic Type specifications. - /// - Parameter contentSizeCategory: An overridden `UIContentSizeCategory` to conform to. - /// - Returns: A `UIFont` for the given token. - @objc(typographyForToken:adjustsForContentSizeCategory:contentSizeCategory:) - func typography(_ token: TypographyToken, - adjustsForContentSizeCategory: Bool = true, - contentSizeCategory: UIContentSizeCategory) -> UIFont { - return UIFont.fluent(typographyTokenSet[token], - shouldScale: adjustsForContentSizeCategory, - contentSizeCategory: contentSizeCategory) + @objc(typographyForToken:) + func typographyInfo(_ token: TypographyToken) -> FontInfo { + return typographyTokenSet[token] } /// Returns an array of colors for the given token. @@ -193,366 +176,366 @@ public extension FluentTheme { /// - Parameter token: The `GradientTokens` value to be retrieved. /// - Returns: An array of `Color` values for the given token. func gradient(_ token: GradientToken) -> [Color] { - return gradientTokenSet[token].map { Color(dynamicColor: $0) } + return gradientTokenSet[token] } } extension FluentTheme { - static func defaultColor(_ token: FluentTheme.ColorToken) -> DynamicColor { + static func defaultColor(_ token: FluentTheme.ColorToken) -> Color { switch token { case .foreground1: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey14), - dark: GlobalTokens.neutralSwiftUIColor(.white)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey14), + dark: GlobalTokens.neutralSwiftUIColor(.white)) case .foreground2: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey38), - dark: GlobalTokens.neutralSwiftUIColor(.grey84)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey38), + dark: GlobalTokens.neutralSwiftUIColor(.grey84)) case .foreground3: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey50), - dark: GlobalTokens.neutralSwiftUIColor(.grey68)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey50), + dark: GlobalTokens.neutralSwiftUIColor(.grey68)) case .foregroundDisabled1: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey74), - dark: GlobalTokens.neutralSwiftUIColor(.grey36)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey74), + dark: GlobalTokens.neutralSwiftUIColor(.grey36)) case .foregroundDisabled2: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.grey18)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.grey18)) case .foregroundOnColor: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.black)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.black)) case .brandForegroundTint: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm60), - dark: GlobalTokens.brandSwiftUIColor(.comm130)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm60), + dark: GlobalTokens.brandSwiftUIColor(.comm130)) case .brandForeground1: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm80), - dark: GlobalTokens.brandSwiftUIColor(.comm100)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm80), + dark: GlobalTokens.brandSwiftUIColor(.comm100)) case .brandForeground1Pressed: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm50), - dark: GlobalTokens.brandSwiftUIColor(.comm140)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm50), + dark: GlobalTokens.brandSwiftUIColor(.comm140)) case .brandForeground1Selected: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm60), - dark: GlobalTokens.brandSwiftUIColor(.comm120)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm60), + dark: GlobalTokens.brandSwiftUIColor(.comm120)) case .brandForegroundDisabled1: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm90)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm90)) case .brandForegroundDisabled2: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm140), - dark: GlobalTokens.brandSwiftUIColor(.comm40)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm140), + dark: GlobalTokens.brandSwiftUIColor(.comm40)) case .brandGradient1: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.gradientPrimaryLight), - dark: GlobalTokens.brandSwiftUIColor(.gradientPrimaryDark)) + return Color(light: GlobalTokens.brandSwiftUIColor(.gradientPrimaryLight), + dark: GlobalTokens.brandSwiftUIColor(.gradientPrimaryDark)) case .brandGradient2: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.gradientSecondaryLight), - dark: GlobalTokens.brandSwiftUIColor(.gradientSecondaryDark)) + return Color(light: GlobalTokens.brandSwiftUIColor(.gradientSecondaryLight), + dark: GlobalTokens.brandSwiftUIColor(.gradientSecondaryDark)) case .brandGradient3: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.gradientTertiaryLight), - dark: GlobalTokens.brandSwiftUIColor(.gradientTertiaryDark)) + return Color(light: GlobalTokens.brandSwiftUIColor(.gradientTertiaryLight), + dark: GlobalTokens.brandSwiftUIColor(.gradientTertiaryDark)) case .foregroundDarkStatic: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.black), - dark: GlobalTokens.neutralSwiftUIColor(.black)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.black), + dark: GlobalTokens.neutralSwiftUIColor(.black)) case .foregroundLightStatic: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.white)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.white)) case .background1: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.black), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey4)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.black), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey4)) case .background1Pressed: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey88), - dark: GlobalTokens.neutralSwiftUIColor(.grey18), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey18)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey88), + dark: GlobalTokens.neutralSwiftUIColor(.grey18), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey18)) case .background1Selected: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey92), - dark: GlobalTokens.neutralSwiftUIColor(.grey14), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey14)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey92), + dark: GlobalTokens.neutralSwiftUIColor(.grey14), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey14)) case .background2: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.grey12), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey16)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.grey12), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey16)) case .background2Pressed: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey88), - dark: GlobalTokens.neutralSwiftUIColor(.grey30), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey30)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey88), + dark: GlobalTokens.neutralSwiftUIColor(.grey30), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey30)) case .background2Selected: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey92), - dark: GlobalTokens.neutralSwiftUIColor(.grey26), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey26)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey92), + dark: GlobalTokens.neutralSwiftUIColor(.grey26), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey26)) case .background3: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.grey16), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey20)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.grey16), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey20)) case .background3Pressed: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey88), - dark: GlobalTokens.neutralSwiftUIColor(.grey34), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey34)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey88), + dark: GlobalTokens.neutralSwiftUIColor(.grey34), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey34)) case .background3Selected: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey92), - dark: GlobalTokens.neutralSwiftUIColor(.grey30), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey30)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey92), + dark: GlobalTokens.neutralSwiftUIColor(.grey30), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey30)) case .background4: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey98), - dark: GlobalTokens.neutralSwiftUIColor(.grey20), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey24)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey98), + dark: GlobalTokens.neutralSwiftUIColor(.grey20), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey24)) case .background4Pressed: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey86), - dark: GlobalTokens.neutralSwiftUIColor(.grey38), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey38)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey86), + dark: GlobalTokens.neutralSwiftUIColor(.grey38), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey38)) case .background4Selected: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey90), - dark: GlobalTokens.neutralSwiftUIColor(.grey34), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey34)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey90), + dark: GlobalTokens.neutralSwiftUIColor(.grey34), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey34)) case .background5: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey94), - dark: GlobalTokens.neutralSwiftUIColor(.grey24), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey28)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey94), + dark: GlobalTokens.neutralSwiftUIColor(.grey24), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey28)) case .background5Pressed: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey82), - dark: GlobalTokens.neutralSwiftUIColor(.grey42), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey42)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey82), + dark: GlobalTokens.neutralSwiftUIColor(.grey42), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey42)) case .background5Selected: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey86), - dark: GlobalTokens.neutralSwiftUIColor(.grey38), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey38)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey86), + dark: GlobalTokens.neutralSwiftUIColor(.grey38), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey38)) case .background6: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey82), - dark: GlobalTokens.neutralSwiftUIColor(.grey36), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey40)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey82), + dark: GlobalTokens.neutralSwiftUIColor(.grey36), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey40)) case .backgroundDisabled: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey88), - dark: GlobalTokens.neutralSwiftUIColor(.grey32), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey32)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey88), + dark: GlobalTokens.neutralSwiftUIColor(.grey32), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey32)) case .brandBackgroundTint: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm150), - dark: GlobalTokens.brandSwiftUIColor(.comm40)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm150), + dark: GlobalTokens.brandSwiftUIColor(.comm40)) case .brandBackground1: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm80), - dark: GlobalTokens.brandSwiftUIColor(.comm100)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm80), + dark: GlobalTokens.brandSwiftUIColor(.comm100)) case .brandBackground1Pressed: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm50), - dark: GlobalTokens.brandSwiftUIColor(.comm140)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm50), + dark: GlobalTokens.brandSwiftUIColor(.comm140)) case .brandBackground1Selected: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm60), - dark: GlobalTokens.brandSwiftUIColor(.comm120)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm60), + dark: GlobalTokens.brandSwiftUIColor(.comm120)) case .brandBackground2: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm70)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm70)) case .brandBackground2Pressed: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm40)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm40)) case .brandBackground2Selected: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm80)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm80)) case .brandBackground3: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm60), - dark: GlobalTokens.brandSwiftUIColor(.comm120)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm60), + dark: GlobalTokens.brandSwiftUIColor(.comm120)) case .brandBackgroundDisabled: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm140), - dark: GlobalTokens.brandSwiftUIColor(.comm40)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm140), + dark: GlobalTokens.brandSwiftUIColor(.comm40)) case .stencil1: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey90), - dark: GlobalTokens.neutralSwiftUIColor(.grey34)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey90), + dark: GlobalTokens.neutralSwiftUIColor(.grey34)) case .stencil2: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey98), - dark: GlobalTokens.neutralSwiftUIColor(.grey20)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey98), + dark: GlobalTokens.neutralSwiftUIColor(.grey20)) case .backgroundCanvas: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey96), - dark: GlobalTokens.neutralSwiftUIColor(.grey8), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey14)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey96), + dark: GlobalTokens.neutralSwiftUIColor(.grey8), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey14)) case .backgroundDarkStatic: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey14), - dark: GlobalTokens.neutralSwiftUIColor(.grey24), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey30)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey14), + dark: GlobalTokens.neutralSwiftUIColor(.grey24), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey30)) case .backgroundInverted: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey46), - dark: GlobalTokens.neutralSwiftUIColor(.grey72), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey78)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey46), + dark: GlobalTokens.neutralSwiftUIColor(.grey72), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey78)) case .backgroundLightStatic: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.white), - darkElevated: GlobalTokens.neutralSwiftUIColor(.white)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.white), + darkElevated: GlobalTokens.neutralSwiftUIColor(.white)) case .backgroundLightStaticDisabled: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.grey68), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey42)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.grey68), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey42)) case .stroke1: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey82), - dark: GlobalTokens.neutralSwiftUIColor(.grey30), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey36)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey82), + dark: GlobalTokens.neutralSwiftUIColor(.grey30), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey36)) case .stroke1Pressed: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey70), - dark: GlobalTokens.neutralSwiftUIColor(.grey48)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey70), + dark: GlobalTokens.neutralSwiftUIColor(.grey48)) case .stroke2: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey88), - dark: GlobalTokens.neutralSwiftUIColor(.grey24), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey30)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey88), + dark: GlobalTokens.neutralSwiftUIColor(.grey24), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey30)) case .strokeAccessible: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey38), - dark: GlobalTokens.neutralSwiftUIColor(.grey62), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey68)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey38), + dark: GlobalTokens.neutralSwiftUIColor(.grey62), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey68)) case .strokeFocus1: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.white), - dark: GlobalTokens.neutralSwiftUIColor(.black)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.white), + dark: GlobalTokens.neutralSwiftUIColor(.black)) case .strokeFocus2: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.black), - dark: GlobalTokens.neutralSwiftUIColor(.white)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.black), + dark: GlobalTokens.neutralSwiftUIColor(.white)) case .strokeDisabled: - return DynamicColor(light: GlobalTokens.neutralSwiftUIColor(.grey88), - dark: GlobalTokens.neutralSwiftUIColor(.grey26), - darkElevated: GlobalTokens.neutralSwiftUIColor(.grey32)) + return Color(light: GlobalTokens.neutralSwiftUIColor(.grey88), + dark: GlobalTokens.neutralSwiftUIColor(.grey26), + darkElevated: GlobalTokens.neutralSwiftUIColor(.grey32)) case .brandStroke1: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm80), - dark: GlobalTokens.brandSwiftUIColor(.comm100)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm80), + dark: GlobalTokens.brandSwiftUIColor(.comm100)) case .brandStroke1Pressed: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm50), - dark: GlobalTokens.brandSwiftUIColor(.comm140)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm50), + dark: GlobalTokens.brandSwiftUIColor(.comm140)) case .brandStroke1Selected: - return DynamicColor(light: GlobalTokens.brandSwiftUIColor(.comm60), - dark: GlobalTokens.brandSwiftUIColor(.comm120)) + return Color(light: GlobalTokens.brandSwiftUIColor(.comm60), + dark: GlobalTokens.brandSwiftUIColor(.comm120)) case .dangerBackground1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.red, .tint60), - dark: GlobalTokens.sharedSwiftUIColor(.red, .shade40)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.red, .tint60), + dark: GlobalTokens.sharedSwiftUIColor(.red, .shade40)) case .dangerBackground2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.red, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.red, .shade10)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.red, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.red, .shade10)) case .dangerForeground1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.red, .shade10), - dark: GlobalTokens.sharedSwiftUIColor(.red, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.red, .shade10), + dark: GlobalTokens.sharedSwiftUIColor(.red, .tint30)) case .dangerForeground2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.red, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.red, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.red, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.red, .tint30)) case .dangerStroke1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.red, .tint20), - dark: GlobalTokens.sharedSwiftUIColor(.red, .tint20)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.red, .tint20), + dark: GlobalTokens.sharedSwiftUIColor(.red, .tint20)) case .dangerStroke2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.red, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.red, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.red, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.red, .tint30)) case .successBackground1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.green, .tint60), - dark: GlobalTokens.sharedSwiftUIColor(.green, .shade40)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.green, .tint60), + dark: GlobalTokens.sharedSwiftUIColor(.green, .shade40)) case .successBackground2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.green, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.green, .shade10)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.green, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.green, .shade10)) case .successForeground1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.green, .shade10), - dark: GlobalTokens.sharedSwiftUIColor(.green, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.green, .shade10), + dark: GlobalTokens.sharedSwiftUIColor(.green, .tint30)) case .successForeground2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.green, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.green, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.green, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.green, .tint30)) case .successStroke1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.green, .tint20), - dark: GlobalTokens.sharedSwiftUIColor(.green, .tint20)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.green, .tint20), + dark: GlobalTokens.sharedSwiftUIColor(.green, .tint20)) case .severeBackground1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint60), - dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .shade40)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint60), + dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .shade40)) case .severeBackground2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .shade10)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .shade10)) case .severeForeground1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .shade10), - dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .shade10), + dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint30)) case .severeForeground2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .shade20), - dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .shade20), + dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint30)) case .severeStroke1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint10), - dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint20)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint10), + dark: GlobalTokens.sharedSwiftUIColor(.darkOrange, .tint20)) case .warningBackground1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.yellow, .tint60), - dark: GlobalTokens.sharedSwiftUIColor(.yellow, .shade40)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.yellow, .tint60), + dark: GlobalTokens.sharedSwiftUIColor(.yellow, .shade40)) case .warningBackground2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.yellow, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.yellow, .shade10)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.yellow, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.yellow, .shade10)) case .warningForeground1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.yellow, .shade30), - dark: GlobalTokens.sharedSwiftUIColor(.yellow, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.yellow, .shade30), + dark: GlobalTokens.sharedSwiftUIColor(.yellow, .tint30)) case .warningForeground2: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.yellow, .shade30), - dark: GlobalTokens.sharedSwiftUIColor(.yellow, .tint30)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.yellow, .shade30), + dark: GlobalTokens.sharedSwiftUIColor(.yellow, .tint30)) case .warningStroke1: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.yellow, .shade30), - dark: GlobalTokens.sharedSwiftUIColor(.yellow, .shade20)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.yellow, .shade30), + dark: GlobalTokens.sharedSwiftUIColor(.yellow, .shade20)) case .presenceAway: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.marigold, .primary)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.marigold, .primary)) case .presenceDnd: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.red, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.red, .tint10)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.red, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.red, .tint10)) case .presenceAvailable: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.lightGreen, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.lightGreen, .tint20)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.lightGreen, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.lightGreen, .tint20)) case .presenceOof: - return DynamicColor(light: GlobalTokens.sharedSwiftUIColor(.berry, .primary), - dark: GlobalTokens.sharedSwiftUIColor(.berry, .tint20)) + return Color(light: GlobalTokens.sharedSwiftUIColor(.berry, .primary), + dark: GlobalTokens.sharedSwiftUIColor(.berry, .tint20)) } } static func defaultShadow(_ token: ShadowToken) -> ShadowInfo { switch token { case .clear: - return ShadowInfo(keyColor: .clear, + return ShadowInfo(keyColor: Color.clear, keyBlur: 0.0, xKey: 0.0, yKey: 0.0, - ambientColor: .clear, + ambientColor: Color.clear, ambientBlur: 0.0, xAmbient: 0.0, yAmbient: 0.0) case .shadow02: - return ShadowInfo(keyColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.14), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.28)), + return ShadowInfo(keyColor: .init(light: .black.opacity(0.14), + dark: .black.opacity(0.28)), keyBlur: 2, xKey: 0, yKey: 1, - ambientColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.12), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.20)), + ambientColor: .init(light: .black.opacity(0.12), + dark: .black.opacity(0.20)), ambientBlur: 2, xAmbient: 0, yAmbient: 0) case .shadow04: - return ShadowInfo(keyColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.14), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.28)), + return ShadowInfo(keyColor: .init(light: .black.opacity(0.14), + dark: .black.opacity(0.28)), keyBlur: 4, xKey: 0, yKey: 2, - ambientColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.12), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.20)), + ambientColor: .init(light: .black.opacity(0.12), + dark: .black.opacity(0.20)), ambientBlur: 2, xAmbient: 0, yAmbient: 0) case .shadow08: - return ShadowInfo(keyColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.14), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.28)), + return ShadowInfo(keyColor: .init(light: .black.opacity(0.14), + dark: .black.opacity(0.28)), keyBlur: 8, xKey: 0, yKey: 4, - ambientColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.12), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.20)), + ambientColor: .init(light: .black.opacity(0.12), + dark: .black.opacity(0.20)), ambientBlur: 2, xAmbient: 0, yAmbient: 0) case .shadow16: - return ShadowInfo(keyColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.14), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.28)), + return ShadowInfo(keyColor: .init(light: .black.opacity(0.14), + dark: .black.opacity(0.28)), keyBlur: 16, xKey: 0, yKey: 8, - ambientColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.12), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.20)), + ambientColor: .init(light: .black.opacity(0.12), + dark: .black.opacity(0.20)), ambientBlur: 2, xAmbient: 0, yAmbient: 0) case .shadow28: - return ShadowInfo(keyColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.24), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.48)), + return ShadowInfo(keyColor: .init(light: .black.opacity(0.24), + dark: .black.opacity(0.48)), keyBlur: 28, xKey: 0, yKey: 14, - ambientColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.20), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.40)), + ambientColor: .init(light: .black.opacity(0.20), + dark: .black.opacity(0.40)), ambientBlur: 8, xAmbient: 0, yAmbient: 0) case .shadow64: - return ShadowInfo(keyColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.24), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.48)), + return ShadowInfo(keyColor: .init(light: .black.opacity(0.24), + dark: .black.opacity(0.48)), keyBlur: 64, xKey: 0, yKey: 32, - ambientColor: UIColor(light: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.20), - dark: UIColor(red: 0.0, green: 0.0, blue: 0.0, alpha: 0.40)), + ambientColor: .init(light: .black.opacity(0.20), + dark: .black.opacity(0.40)), ambientBlur: 8, xAmbient: 0, yAmbient: 0) @@ -601,7 +584,7 @@ extension FluentTheme { } /// Derives its default values from the theme's `colorTokenSet` values - static func defaultGradientColor(_ token: GradientToken, colorTokenSet: TokenSet) -> [DynamicColor] { + static func defaultGradientColor(_ token: GradientToken, colorTokenSet: TokenSet) -> [Color] { switch token { case .flair: return [colorTokenSet[.brandGradient1], diff --git a/Sources/FluentUI_iOS/Core/Theme/FluentTheme+UIKit.swift b/Sources/FluentUI_iOS/Core/Theme/FluentTheme+UIKit.swift index ff201281ac..ad7976429e 100644 --- a/Sources/FluentUI_iOS/Core/Theme/FluentTheme+UIKit.swift +++ b/Sources/FluentUI_iOS/Core/Theme/FluentTheme+UIKit.swift @@ -8,13 +8,77 @@ import SwiftUI public extension FluentTheme { + /// Initializes and returns a new `FluentTheme`. + /// + /// A `FluentTheme` receives any custom alias tokens on initialization via arguments here. + /// Control tokens can be customized via `register(controlType:tokens:) `; + /// see that method's description for additional information. + /// + /// - Parameters: + /// - colorOverrides: A `Dictionary` of override values mapped to `ColorTokens`. + /// - shadowOverrides: A `Dictionary` of override values mapped to `ShadowTokens`. + /// - typographyOverrides: A `Dictionary` of override values mapped to `TypographyTokens`. + /// - gradientOverrides: A `Dictionary` of override values mapped to `GradientTokens`. + /// + /// - Returns: An initialized `FluentTheme` instance, with optional overrides. + convenience init(colorOverrides: [ColorToken: UIColor]? = nil, + shadowOverrides: [ShadowToken: ShadowInfo]? = nil, + typographyOverrides: [TypographyToken: UIFont]? = nil, + gradientOverrides: [GradientToken: [UIColor]]? = nil) { + + let mappedColorOverrides = colorOverrides?.compactMapValues({ uiColor in + return Color(uiColor) + }) + + let mappedTypographyOverrides = typographyOverrides?.compactMapValues({ uiFont in + return FontInfo(name: uiFont.fontName, size: uiFont.pointSize) + }) + + let mappedGradientOverrides = gradientOverrides?.compactMapValues({ uiColors in + return uiColors.compactMap { uiColor in + Color(uiColor) + } + }) + + self.init(colorOverrides: mappedColorOverrides, + shadowOverrides: shadowOverrides, + typographyOverrides: mappedTypographyOverrides, + gradientOverrides: mappedGradientOverrides) + } + /// Returns the color value for the given token. /// /// - Parameter token: The `ColorsTokens` value to be retrieved. /// - Returns: A `UIColor` for the given token. @objc(colorForToken:) func color(_ token: ColorToken) -> UIColor { - return UIColor(dynamicColor: colorTokenSet[token]) + return UIColor(swiftUIColor(token)) + } + + /// Returns the font value for the given token. + /// + /// - Parameter token: The `TypographyTokens` value to be retrieved. + /// - Parameter adjustsForContentSizeCategory: If true, the resulting font will change size according to Dynamic Type specifications. + /// - Returns: A `UIFont` for the given token. + @objc(typographyForToken:adjustsForContentSizeCategory:) + func typography(_ token: TypographyToken, adjustsForContentSizeCategory: Bool = true) -> UIFont { + return UIFont.fluent(typographyInfo(token), + shouldScale: adjustsForContentSizeCategory) + } + + /// Returns the font value for the given token. + /// + /// - Parameter token: The `TypographyTokens` value to be retrieved. + /// - Parameter adjustsForContentSizeCategory: If true, the resulting font will change size according to Dynamic Type specifications. + /// - Parameter contentSizeCategory: An overridden `UIContentSizeCategory` to conform to. + /// - Returns: A `UIFont` for the given token. + @objc(typographyForToken:adjustsForContentSizeCategory:contentSizeCategory:) + func typography(_ token: TypographyToken, + adjustsForContentSizeCategory: Bool = true, + contentSizeCategory: UIContentSizeCategory) -> UIFont { + return UIFont.fluent(typographyInfo(token), + shouldScale: adjustsForContentSizeCategory, + contentSizeCategory: contentSizeCategory) } /// Returns an array of colors for the given token. @@ -23,6 +87,59 @@ public extension FluentTheme { /// - Returns: An array of `UIColor` values for the given token. @objc(gradientColorsForToken:) func gradient(_ token: GradientToken) -> [UIColor] { - return gradientTokenSet[token].map { UIColor(dynamicColor: $0) } + let colors: [Color] = gradient(token) + return colors.map { UIColor($0) } + } +} + +@objc extension UIView: FluentThemeable { + private struct Keys { + static var fluentTheme: UInt8 = 0 + static var cachedFluentTheme: UInt8 = 0 + } + + /// The custom `FluentTheme` to apply to this view. + @objc public var fluentTheme: FluentTheme { + get { + var optionalView: UIView? = self + while let view = optionalView { + // If we successfully find a theme, return it. + if let theme = objc_getAssociatedObject(view, &Keys.fluentTheme) as? FluentTheme { + return theme + } else { + optionalView = view.superview + } + } + + // No custom themes anywhere, so return the default theme + return FluentTheme.shared + } + set { + objc_setAssociatedObject(self, &Keys.fluentTheme, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + NotificationCenter.default.post(name: .didChangeTheme, object: self) + } + } + + /// Removes any associated `ColorProvider` from the given `UIView`. + @objc(resetFluentTheme) + public func resetFluentTheme() { + objc_setAssociatedObject(self, &Keys.fluentTheme, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + NotificationCenter.default.post(name: .didChangeTheme, object: self) + } + + @objc(isApplicableThemeChange:) + public func isApplicableThemeChange(_ notification: Notification) -> Bool { + // Do not update unless the notification's name is `.didChangeTheme`. + guard notification.name == .didChangeTheme else { + return false + } + + // If there is no object, or it is not a UIView, we must assume that we need to update. + guard let themeView = notification.object as? UIView else { + return true + } + + // If the object is a UIView, we only update if `view` is a descendant thereof. + return self.isDescendant(of: themeView) } } diff --git a/Sources/FluentUI_iOS/Core/FluentTheme.swift b/Sources/FluentUI_iOS/Core/Theme/FluentTheme.swift similarity index 63% rename from Sources/FluentUI_iOS/Core/FluentTheme.swift rename to Sources/FluentUI_iOS/Core/Theme/FluentTheme.swift index 0d7ad39d48..ce261a69a3 100644 --- a/Sources/FluentUI_iOS/Core/FluentTheme.swift +++ b/Sources/FluentUI_iOS/Core/Theme/FluentTheme.swift @@ -16,7 +16,10 @@ public class FluentTheme: NSObject, ObservableObject { /// /// - Returns: An initialized `FluentTheme` instance, with optional overrides. @objc public convenience override init() { - self.init(colorOverrides: nil, shadowOverrides: nil, typographyOverrides: nil, gradientOverrides: nil) + self.init(colorOverrides: nil as [ColorToken: Color]?, + shadowOverrides: nil as [ShadowToken: ShadowInfo]?, + typographyOverrides: nil as [TypographyToken: FontInfo]?, + gradientOverrides: nil as [GradientToken: [Color]]?) } /// Initializes and returns a new `FluentTheme`. @@ -32,34 +35,24 @@ public class FluentTheme: NSObject, ObservableObject { /// - gradientOverrides: A `Dictionary` of override values mapped to `GradientTokens`. /// /// - Returns: An initialized `FluentTheme` instance, with optional overrides. - public init(colorOverrides: [ColorToken: UIColor]? = nil, + public init(colorOverrides: [ColorToken: Color]? = nil, shadowOverrides: [ShadowToken: ShadowInfo]? = nil, - typographyOverrides: [TypographyToken: UIFont]? = nil, - gradientOverrides: [GradientToken: [UIColor]]? = nil) { - - // Need to massage UIFonts into FontInfo objects - let mappedTypographyOverrides = typographyOverrides?.compactMapValues({ font in - return FontInfo(name: font.fontName, size: font.pointSize) - }) - - let mappedColorOverrides = colorOverrides?.compactMapValues({ color in - return DynamicColor(uiColor: color) - }) - + typographyOverrides: [TypographyToken: FontInfo]? = nil, + gradientOverrides: [GradientToken: [Color]]? = nil) { #if os(visionOS) // We have custom overrides for `defaultColors` in visionOS. - let defaultColorFunction: ((FluentTheme.ColorToken) -> DynamicColor) = FluentTheme.defaultColor_visionOS(_:) + let defaultColorFunction: ((FluentTheme.ColorToken) -> Color) = FluentTheme.defaultColor_visionOS(_:) #else - let defaultColorFunction: ((FluentTheme.ColorToken) -> DynamicColor) = FluentTheme.defaultColor(_:) + let defaultColorFunction: ((FluentTheme.ColorToken) -> Color) = FluentTheme.defaultColor(_:) #endif - let colorTokenSet = TokenSet(defaultColorFunction, mappedColorOverrides) + let colorTokenSet = TokenSet(defaultColorFunction, colorOverrides) let shadowTokenSet = TokenSet(FluentTheme.defaultShadow(_:), shadowOverrides) - let typographyTokenSet = TokenSet(FluentTheme.defaultTypography(_:), mappedTypographyOverrides) - let gradientTokenSet = TokenSet({ [colorTokenSet] token in + let typographyTokenSet = TokenSet(FluentTheme.defaultTypography(_:), typographyOverrides) + let gradientTokenSet = TokenSet({ [colorTokenSet] token in // Reference the colorTokenSet as part of the gradient lookup return FluentTheme.defaultGradientColor(token, colorTokenSet: colorTokenSet) - }) + }, gradientOverrides) self.colorTokenSet = colorTokenSet self.shadowTokenSet = shadowTokenSet @@ -101,34 +94,23 @@ public class FluentTheme: NSObject, ObservableObject { } } - /// Determines if a given `Notification` should cause an update for the given `UIView`. + /// Determines if a given `Notification` should cause an update for the given `FluentThemeable`. /// /// - Parameter notification: A `Notification` object that may be requesting a view update based on a theme change. - /// - Parameter view: The `UIView` instance that wants to determine whether to update. + /// - Parameter view: The `FluentThemeable` instance that wants to determine whether to update. /// - /// - Returns: `True` if the view should update, `false` otherwise. + /// - Returns: `true` if the view should update, `false` otherwise. @objc(isApplicableThemeChangeNotification:forView:) public static func isApplicableThemeChange(_ notification: Notification, - for view: UIView) -> Bool { - // Do not update unless the notification's name is `.didChangeTheme`. - guard notification.name == .didChangeTheme else { - return false - } - - // If there is no object, or it is not a UIView, we must assume that we need to update. - guard let themeView = notification.object as? UIView else { - return true - } - - // If the object is a UIView, we only update if `view` is a descendant thereof. - return view.isDescendant(of: themeView) + for view: FluentThemeable) -> Bool { + return view.isApplicableThemeChange(notification) } // Token storage - let colorTokenSet: TokenSet + let colorTokenSet: TokenSet let shadowTokenSet: TokenSet let typographyTokenSet: TokenSet - let gradientTokenSet: TokenSet + let gradientTokenSet: TokenSet private func tokenKey(_ tokenSetType: ControlTokenSet.Type) -> String { return "\(tokenSetType)" @@ -142,6 +124,7 @@ public class FluentTheme: NSObject, ObservableObject { /// Public protocol that, when implemented, allows any container to store and yield a `FluentTheme`. @objc public protocol FluentThemeable { var fluentTheme: FluentTheme { get set } + func isApplicableThemeChange(_ notification: Notification) -> Bool } public extension Notification.Name { @@ -152,42 +135,6 @@ public extension Notification.Name { static let didChangeTheme = Notification.Name("FluentUI.stylesheet.theme") } -@objc extension UIView: FluentThemeable { - private struct Keys { - static var fluentTheme: UInt8 = 0 - static var cachedFluentTheme: UInt8 = 0 - } - - /// The custom `FluentTheme` to apply to this view. - @objc public var fluentTheme: FluentTheme { - get { - var optionalView: UIView? = self - while let view = optionalView { - // If we successfully find a theme, return it. - if let theme = objc_getAssociatedObject(view, &Keys.fluentTheme) as? FluentTheme { - return theme - } else { - optionalView = view.superview - } - } - - // No custom themes anywhere, so return the default theme - return FluentTheme.shared - } - set { - objc_setAssociatedObject(self, &Keys.fluentTheme, newValue, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - NotificationCenter.default.post(name: .didChangeTheme, object: self) - } - } - - /// Removes any associated `ColorProvider` from the given `UIView`. - @objc(resetFluentTheme) - public func resetFluentTheme() { - objc_setAssociatedObject(self, &Keys.fluentTheme, nil, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) - NotificationCenter.default.post(name: .didChangeTheme, object: self) - } -} - // MARK: - Environment public extension View { diff --git a/Sources/FluentUI_iOS/Core/Theme/Tokens/ControlTokenSet.swift b/Sources/FluentUI_iOS/Core/Theme/Tokens/ControlTokenSet.swift index 00e8225970..605bba1daa 100644 --- a/Sources/FluentUI_iOS/Core/Theme/Tokens/ControlTokenSet.swift +++ b/Sources/FluentUI_iOS/Core/Theme/Tokens/ControlTokenSet.swift @@ -4,11 +4,13 @@ // import Combine -import UIKit import SwiftUI +#if os(iOS) || os(visionOS) +import UIKit +#endif // os(iOS) || os(visionOS) /// Base class for all Fluent control tokenization. -public class ControlTokenSet: ObservableObject { +open class ControlTokenSet: ObservableObject { /// Allows us to index into this token set using square brackets. /// /// We can use square brackets to both read and write into this `TokenSet`. For example: @@ -61,7 +63,7 @@ public class ControlTokenSet: ObservableObject { /// - Parameter otherTokenSet: The token set we will be pulling values from. /// - Parameter mapping: A `Dictionary` that maps our own tokens that we wish to override with /// their corresponding tokens in `otherTokenSet`. - func setOverrides(from otherTokenSet: ControlTokenSet, mapping: [T: U]) { + public func setOverrides(from otherTokenSet: ControlTokenSet, mapping: [T: U]) { // Make a copy so we write all the values at once var valueOverrideCopy = valueOverrides ?? [:] mapping.forEach { (thisToken, otherToken) in @@ -71,7 +73,7 @@ public class ControlTokenSet: ObservableObject { } /// Initialize the `ControlTokenSet` with an escaping callback for fetching default values. - init(_ defaults: @escaping (_ token: T, _ theme: FluentTheme) -> ControlTokenValue) { + public init(_ defaults: @escaping (_ token: T, _ theme: FluentTheme) -> ControlTokenValue) { self.defaults = defaults } @@ -81,7 +83,7 @@ public class ControlTokenSet: ObservableObject { /// Removes all `onUpdate`-based observing. Useful if you are re-registering the same tokenSet /// for a new instance of a control (see `Tooltip` for an example). - func deregisterOnUpdate() { + public func deregisterOnUpdate() { if let notificationObserver { NotificationCenter.default.removeObserver(notificationObserver, name: .didChangeTheme, @@ -95,7 +97,7 @@ public class ControlTokenSet: ObservableObject { /// Prepares this token set by installing the current `FluentTheme` if it has changed. /// /// - Parameter fluentTheme: The current `FluentTheme` for the control's environment. - func update(_ fluentTheme: FluentTheme) { + public func update(_ fluentTheme: FluentTheme) { if fluentTheme != self.fluentTheme { self.fluentTheme = fluentTheme } @@ -111,7 +113,7 @@ public class ControlTokenSet: ObservableObject { /// - Parameter token: The token key to fetch any existing override for. /// /// - Returns: the active override value for a given token, or nil if none exists. - func overrideValue(forToken token: T) -> ControlTokenValue? { + public func overrideValue(forToken token: T) -> ControlTokenValue? { if let value = valueOverrides?[token] { return value } else if let value = fluentTheme.tokens(for: type(of: self))?[token] { @@ -124,7 +126,7 @@ public class ControlTokenSet: ObservableObject { /// /// - Parameter value: The value to set as an override. /// - Parameter token: The token key whose value should be set. - func setOverrideValue(_ value: ControlTokenValue?, forToken token: T) { + public func setOverrideValue(_ value: ControlTokenValue?, forToken token: T) { if valueOverrides == nil { valueOverrides = [:] } @@ -142,7 +144,7 @@ public class ControlTokenSet: ObservableObject { /// /// - Parameter control: The `UIView` instance that wishes to observe. /// - Parameter onUpdate: A callback to run whenever `control` should update itself. - func registerOnUpdate(for control: UIView, onUpdate: @escaping (() -> Void)) { + public func registerOnUpdate(for control: FluentThemeable, onUpdate: @escaping (() -> Void)) { guard self.onUpdate == nil, changeSink == nil, notificationObserver == nil else { @@ -164,7 +166,7 @@ public class ControlTokenSet: ObservableObject { queue: nil) { [weak self, weak control] notification in guard let strongSelf = self, let control, - FluentTheme.isApplicableThemeChange(notification, for: control) + control.isApplicableThemeChange(notification) else { return } @@ -173,7 +175,7 @@ public class ControlTokenSet: ObservableObject { } /// The current `FluentTheme` associated with this `ControlTokenSet`. - var fluentTheme: FluentTheme = FluentTheme.shared { + public var fluentTheme: FluentTheme = FluentTheme.shared { didSet { guard let onUpdate else { return @@ -204,9 +206,11 @@ public class ControlTokenSet: ObservableObject { /// Union-type enumeration of all possible token values to be stored by a `ControlTokenSet`. public enum ControlTokenValue { case float(() -> CGFloat) - case uiColor(() -> UIColor) case color(() -> Color) +#if os(iOS) || os(visionOS) + case uiColor(() -> UIColor) case uiFont(() -> UIFont) +#endif // os(iOS) || os(visionOS) case shadowInfo(() -> ShadowInfo) public var float: CGFloat { @@ -218,6 +222,21 @@ public enum ControlTokenValue { } } + public var color: Color { + if case .color(let color) = self { + return color() + } else { +#if os(iOS) || os(visionOS) + if case .uiColor(let uiColor) = self { + return Color(uiColor()) + } +#endif // os(iOS) || os(visionOS) + assertionFailure("Cannot convert token to Color: \(self)") + return fallbackColor + } + } + +#if os(iOS) || os(visionOS) public var uiColor: UIColor { if case .uiColor(let uiColor) = self { return uiColor() @@ -229,17 +248,6 @@ public enum ControlTokenValue { } } - public var color: Color { - if case .color(let color) = self { - return color() - } else if case .uiColor(let uiColor) = self { - return Color(uiColor()) - } else { - assertionFailure("Cannot convert token to Color: \(self)") - return fallbackColor - } - } - public var uiFont: UIFont { if case .uiFont(let uiFont) = self { return uiFont() @@ -248,17 +256,18 @@ public enum ControlTokenValue { return UIFont() } } +#endif // os(iOS) || os(visionOS) public var shadowInfo: ShadowInfo { if case .shadowInfo(let shadowInfo) = self { return shadowInfo() } else { assertionFailure("Cannot convert token to ShadowInfo: \(self)") - return ShadowInfo(keyColor: fallbackUIColor, + return ShadowInfo(keyColor: fallbackColor, keyBlur: 10.0, xKey: 10.0, yKey: 10.0, - ambientColor: fallbackUIColor, + ambientColor: fallbackColor, ambientBlur: 10.0, xAmbient: 10.0, yAmbient: 10.0) @@ -284,10 +293,14 @@ public enum ControlTokenValue { switch value { case let number as NSNumber: self = .float { CGFloat(number.doubleValue) } - case let color as UIColor: - self = .uiColor { color } + case let color as Color: + self = .color { color } +#if os(iOS) || os(visionOS) + case let uiColor as UIColor: + self = .uiColor { uiColor } case let font as UIFont: self = .uiFont { font } +#endif // os(iOS) || os(visionOS) case let shadowInfo as ShadowInfo: self = .shadowInfo { shadowInfo } default: @@ -297,14 +310,16 @@ public enum ControlTokenValue { // MARK: - Helpers +#if os(iOS) || os(visionOS) private var fallbackUIColor: UIColor { #if DEBUG // Use our global "Hot Pink" in debug builds, to help identify unintentional conversions. - return GlobalTokens.sharedColor(.hotPink, .primary) + return UIColor(GlobalTokens.sharedSwiftUIColor(.hotPink, .primary)) #else return GlobalTokens.neutralColor(.black) #endif } +#endif // os(iOS) || os(visionOS) private var fallbackColor: Color { #if DEBUG @@ -323,12 +338,14 @@ extension ControlTokenValue: CustomStringConvertible { switch self { case .float(let float): return "ControlTokenValue.float (\(float())" - case .uiColor(let uiColor): - return "ControlTokenValue.uiColor (\(uiColor())" case .color(let color): return "ControlTokenValue.color (\(color())" +#if os(iOS) || os(visionOS) + case .uiColor(let uiColor): + return "ControlTokenValue.uiColor (\(uiColor())" case .uiFont(let uiFont): return "ControlTokenValue.uiFont (\(uiFont())" +#endif // os(iOS) || os(visionOS) case .shadowInfo(let shadowInfo): return "ControlTokenValue.shadowInfo (\(shadowInfo())" } diff --git a/Sources/FluentUI_iOS/Core/Theme/Tokens/DynamicColor.swift b/Sources/FluentUI_iOS/Core/Theme/Tokens/DynamicColor.swift new file mode 100644 index 0000000000..5f52b3c091 --- /dev/null +++ b/Sources/FluentUI_iOS/Core/Theme/Tokens/DynamicColor.swift @@ -0,0 +1,44 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +import SwiftUI + +/// A container that stores a dynamic set of `Color` values. +struct DynamicColor: Hashable { + + /// Creates a custom `ShapeStyle` that stores a dynamic set of `Color` values. + /// + /// - Parameter light: The default `Color` for a light context. Required. + /// - Parameter dark: The override `Color` for a dark context. Optional. + /// - Parameter darkElevated: The override `Color` for a dark elevated context. Optional. + init(light: Color, + dark: Color? = nil, + darkElevated: Color? = nil) { + self.light = light + self.dark = dark + self.darkElevated = darkElevated + } + + let light: Color + let dark: Color? + let darkElevated: Color? +} + +@available(iOS 17, macOS 14, *) +extension DynamicColor: ShapeStyle { + /// Evaluate to a resolved `Color` (in the form of a `ShapeStyle`) given the current `environment`. + func resolve(in environment: EnvironmentValues) -> Color.Resolved { + if environment.colorScheme == .dark { + if environment.isPresented, let darkElevated = darkElevated { + return darkElevated.resolve(in: environment) + } else if let dark = dark { + return dark.resolve(in: environment) + } + } + + // default + return light.resolve(in: environment) + } +} diff --git a/Sources/FluentUI_iOS/Core/Theme/Tokens/EmptyTokenSet.swift b/Sources/FluentUI_iOS/Core/Theme/Tokens/EmptyTokenSet.swift index e4f97e6f46..fbcb53aa9c 100644 --- a/Sources/FluentUI_iOS/Core/Theme/Tokens/EmptyTokenSet.swift +++ b/Sources/FluentUI_iOS/Core/Theme/Tokens/EmptyTokenSet.swift @@ -13,7 +13,7 @@ public enum EmptyToken: Int, TokenSetKey { /// of being tokenized, but are not fully at that stage yet. public class EmptyTokenSet: ControlTokenSet { - init() { + public init() { super.init { _, _ in preconditionFailure("Should not fetch values") } diff --git a/Sources/FluentUI_iOS/Core/Theme/Tokens/FontInfo.swift b/Sources/FluentUI_iOS/Core/Theme/Tokens/FontInfo.swift index cd8f6ae968..f8333de3fd 100644 --- a/Sources/FluentUI_iOS/Core/Theme/Tokens/FontInfo.swift +++ b/Sources/FluentUI_iOS/Core/Theme/Tokens/FontInfo.swift @@ -35,7 +35,7 @@ public class FontInfo: NSObject { /// The weight to use for the font. public let weight: Font.Weight - var textStyle: Font.TextStyle { + public var textStyle: Font.TextStyle { // Defaults to smallest supported text style for mapping, before checking if we're bigger. var textStyle = Font.TextStyle.caption2 for tuple in Self.sizeTuples { @@ -47,7 +47,11 @@ public class FontInfo: NSObject { return textStyle } - fileprivate static var sizeTuples: [(size: CGFloat, textStyle: Font.TextStyle)] = [ + public var matchesSystemSize: Bool { + return FontInfo.sizeTuples.contains(where: { $0.size == size }) + } + + private static var sizeTuples: [(size: CGFloat, textStyle: Font.TextStyle)] = [ (34.0, .largeTitle), (28.0, .title), (22.0, .title2), @@ -62,133 +66,3 @@ public class FontInfo: NSObject { (11.0, .caption2) ] } - -// MARK: - ViewModifier - -public extension Font { - static func fluent(_ fontInfo: FontInfo, shouldScale: Bool = true) -> Font { - // SwiftUI Font is missing some of the ease of construction available in UIFont. - // So just leverage the logic there to create the equivalent SwiftUI font. - let uiFont = UIFont.fluent(fontInfo, shouldScale: shouldScale) - return Font(uiFont) - } -} - -extension UIFont { - @objc public static func fluent(_ fontInfo: FontInfo, shouldScale: Bool = true) -> UIFont { - return fluent(fontInfo, shouldScale: shouldScale, contentSizeCategory: nil) - } - - @objc public static func fluent(_ fontInfo: FontInfo, shouldScale: Bool = true, contentSizeCategory: UIContentSizeCategory?) -> UIFont { - let traitCollection: UITraitCollection? - if let contentSizeCategory = contentSizeCategory { - traitCollection = .init(preferredContentSizeCategory: contentSizeCategory) - } else { - traitCollection = nil - } - - let weight = uiWeight(fontInfo.weight) - - if let name = fontInfo.name, - let font = UIFont(name: name, size: fontInfo.size) { - // Named font - let unscaledFont = font.withWeight(weight) - if shouldScale { - let fontMetrics = UIFontMetrics(forTextStyle: uiTextStyle(fontInfo.textStyle)) - return fontMetrics.scaledFont(for: unscaledFont, compatibleWith: traitCollection) - } else { - return unscaledFont - } - } else { - // System font - if !shouldScale { - return .systemFont(ofSize: fontInfo.size, weight: weight) - } - - let textStyle = uiTextStyle(fontInfo.textStyle) - if FontInfo.sizeTuples.contains(where: { $0.size == fontInfo.size }) { - // System-recognized font size, let the OS scale it for us - return UIFont.preferredFont(forTextStyle: textStyle, compatibleWith: traitCollection).withWeight(weight) - } - - // Custom font size, we need to scale it ourselves - let fontMetrics = UIFontMetrics(forTextStyle: textStyle) - return fontMetrics.scaledFont(for: .systemFont(ofSize: fontInfo.size, weight: weight), compatibleWith: traitCollection) - } - } - - private func withWeight(_ weight: UIFont.Weight) -> UIFont { - var attributes = fontDescriptor.fontAttributes - var traits = (attributes[.traits] as? [UIFontDescriptor.TraitKey: Any]) ?? [:] - - traits[.weight] = weight - - // We need to remove `.name` since it may clash with the requested font weight, but - // `.family` will ensure that e.g. Helvetica stays Helvetica. - attributes[.name] = nil - attributes[.traits] = traits - attributes[.family] = familyName - - let descriptor = UIFontDescriptor(fontAttributes: attributes) - - return UIFont(descriptor: descriptor, size: pointSize) - } - - private static func uiTextStyle(_ textStyle: Font.TextStyle) -> UIFont.TextStyle { - switch textStyle { - case .largeTitle: - return .largeTitle - case .title: - return .title1 - case .title2: - return .title2 - case .title3: - return .title3 - case .headline: - return .headline - case .body: - return .body - case .callout: - return .callout - case .subheadline: - return .subheadline - case .footnote: - return .footnote - case .caption: - return .caption1 - case .caption2: - return .caption2 - default: - // Font.TextStyle has `@unknown default` attribute, so we need a default. - assertionFailure("Unknown Font.TextStyle found! Reverting to .body style.") - return .body - } - } - - private static func uiWeight(_ weight: Font.Weight) -> UIFont.Weight { - switch weight { - case .ultraLight: - return .ultraLight - case .thin: - return .thin - case .light: - return .light - case .regular: - return .regular - case .medium: - return .medium - case .semibold: - return .semibold - case .bold: - return .bold - case .heavy: - return .heavy - case .black: - return .black - default: - // Font.Weight has `@unknown default` attribute, so we need a default. - assertionFailure("Unknown Font.Weight found! Reverting to .regular weight.") - return .regular - } - } -} diff --git a/Sources/FluentUI_iOS/Core/Theme/Tokens/ShadowInfo+UIKit.swift b/Sources/FluentUI_iOS/Core/Theme/Tokens/ShadowInfo+UIKit.swift new file mode 100644 index 0000000000..a4edaf211b --- /dev/null +++ b/Sources/FluentUI_iOS/Core/Theme/Tokens/ShadowInfo+UIKit.swift @@ -0,0 +1,85 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// + +import SwiftUI +import UIKit + +public extension ShadowInfo { + + /// Initializes a shadow struct to be used in Fluent. + /// + /// - Parameters: + /// - keyColor: The color of the key shadow. + /// - keyBlur: The blur of the key shadow. + /// - xKey: The horizontal offset of the key shadow. + /// - yKey: The vertical offset of the key shadow. + /// - ambientColor: The color of the ambient shadow. + /// - ambientBlur: The blur of the ambient shadow. + /// - xAmbient: The horizontal offset of the ambient shadow. + /// - yAmbient: The vertical offset of the ambient shadow. + @objc convenience init(keyColor: UIColor, + keyBlur: CGFloat, + xKey: CGFloat, + yKey: CGFloat, + ambientColor: UIColor, + ambientBlur: CGFloat, + xAmbient: CGFloat, + yAmbient: CGFloat) { + self.init(keyColor: Color(keyColor), + keyBlur: keyBlur, + xKey: xKey, + yKey: yKey, + ambientColor: Color(ambientColor), + ambientBlur: ambientBlur, + xAmbient: xAmbient, + yAmbient: yAmbient) + } + + /// Applies a key and an ambient shadow on a `UIView`. + /// - Parameters: + /// - view: The view on which the shadows will be applied. + /// - parentController: The view controller responsible for the view on which the shadows will be applied. + func applyShadow(to view: UIView, parentController: UIViewController? = nil) { + guard var shadowable = (view as? Shadowable) ?? (view.superview as? Shadowable) ?? (parentController as? Shadowable) else { + assertionFailure("Cannot apply Fluent shadows to a non-Shadowable view") + return + } + + shadowable.ambientShadow?.removeFromSuperlayer() + shadowable.keyShadow?.removeFromSuperlayer() + + let ambientShadow = initializeShadowLayer(view: view, isAmbientShadow: true) + let keyShadow = initializeShadowLayer(view: view) + + shadowable.ambientShadow = ambientShadow + shadowable.keyShadow = keyShadow + + view.layer.insertSublayer(ambientShadow, at: 0) + view.layer.insertSublayer(keyShadow, below: ambientShadow) + } + + /// Initializes a `CALayer` with the relevant `ShadowInfo` values. + /// - Parameters: + /// - view: The view on which the shadow layer will be applied. + /// - isAmbientShadow: Determines whether to apply ambient or key shadow values on the layer. + /// - Returns: The modified `CALayer`. + func initializeShadowLayer(view: UIView, isAmbientShadow: Bool = false) -> CALayer { + let layer = CALayer() + + layer.frame = view.bounds + layer.shadowColor = UIColor(isAmbientShadow ? ambientColor : keyColor).cgColor + layer.shadowRadius = isAmbientShadow ? ambientBlur : keyBlur + + // The shadowOpacity needs to be set to 1 since the alpha is already set through shadowColor + layer.shadowOpacity = 1 + layer.shadowOffset = CGSize(width: isAmbientShadow ? xAmbient : xKey, + height: isAmbientShadow ? yAmbient : yKey) + layer.needsDisplayOnBoundsChange = true + layer.cornerRadius = view.layer.cornerRadius + layer.backgroundColor = view.backgroundColor?.cgColor + + return layer + } +} diff --git a/Sources/FluentUI_iOS/Core/Theme/Tokens/ShadowInfo.swift b/Sources/FluentUI_iOS/Core/Theme/Tokens/ShadowInfo.swift index c038adcff7..5b2c723d95 100644 --- a/Sources/FluentUI_iOS/Core/Theme/Tokens/ShadowInfo.swift +++ b/Sources/FluentUI_iOS/Core/Theme/Tokens/ShadowInfo.swift @@ -4,7 +4,8 @@ // import CoreGraphics -import UIKit +import QuartzCore +import SwiftUI /// Represents a two-part shadow as used by FluentUI. @objc(MSFShadowInfo) @@ -20,11 +21,11 @@ public class ShadowInfo: NSObject { /// - ambientBlur: The blur of the ambient shadow. /// - xAmbient: The horizontal offset of the ambient shadow. /// - yAmbient: The vertical offset of the ambient shadow. - public init(keyColor: UIColor, + public init(keyColor: Color, keyBlur: CGFloat, xKey: CGFloat, yKey: CGFloat, - ambientColor: UIColor, + ambientColor: Color, ambientBlur: CGFloat, xAmbient: CGFloat, yAmbient: CGFloat) { @@ -39,7 +40,7 @@ public class ShadowInfo: NSObject { } /// The color of the key shadow. - @objc public let keyColor: UIColor + public let keyColor: Color /// The blur of the key shadow. @objc public let keyBlur: CGFloat @@ -51,7 +52,7 @@ public class ShadowInfo: NSObject { @objc public let yKey: CGFloat /// The color of the ambient shadow. - @objc public let ambientColor: UIColor + public let ambientColor: Color /// The blur of the ambient shadow. @objc public let ambientBlur: CGFloat @@ -66,55 +67,7 @@ public class ShadowInfo: NSObject { private let shadowBlurAdjustment: CGFloat = 0.5 } -public extension ShadowInfo { - /// Applies a key and an ambient shadow on a `UIView`. - /// - Parameters: - /// - view: The view on which the shadows will be applied. - /// - parentController: The view controller responsible for the view on which the shadows will be applied. - func applyShadow(to view: UIView, parentController: UIViewController? = nil) { - guard var shadowable = (view as? Shadowable) ?? (view.superview as? Shadowable) ?? (parentController as? Shadowable) else { - assertionFailure("Cannot apply Fluent shadows to a non-Shadowable view") - return - } - - shadowable.ambientShadow?.removeFromSuperlayer() - shadowable.keyShadow?.removeFromSuperlayer() - - let ambientShadow = initializeShadowLayer(view: view, isAmbientShadow: true) - let keyShadow = initializeShadowLayer(view: view) - - shadowable.ambientShadow = ambientShadow - shadowable.keyShadow = keyShadow - - view.layer.insertSublayer(ambientShadow, at: 0) - view.layer.insertSublayer(keyShadow, below: ambientShadow) - } - - /// Initializes a `CALayer` with the relevant `ShadowInfo` values. - /// - Parameters: - /// - view: The view on which the shadow layer will be applied. - /// - isAmbientShadow: Determines whether to apply ambient or key shadow values on the layer. - /// - Returns: The modified `CALayer`. - func initializeShadowLayer(view: UIView, isAmbientShadow: Bool = false) -> CALayer { - let layer = CALayer() - - layer.frame = view.bounds - layer.shadowColor = (isAmbientShadow ? ambientColor : keyColor).cgColor - layer.shadowRadius = isAmbientShadow ? ambientBlur : keyBlur - - // The shadowOpacity needs to be set to 1 since the alpha is already set through shadowColor - layer.shadowOpacity = 1 - layer.shadowOffset = CGSize(width: isAmbientShadow ? xAmbient : xKey, - height: isAmbientShadow ? yAmbient : yKey) - layer.needsDisplayOnBoundsChange = true - layer.cornerRadius = view.layer.cornerRadius - layer.backgroundColor = view.backgroundColor?.cgColor - - return layer - } -} - -/// Public protocol that, when implemented, allows any UIView or one of its subviews to implement fluent shadows +/// Public protocol that, when implemented, allows any view or one of its subviews to implement fluent shadows public protocol Shadowable { /// The layer on which the ambient shadow is implemented var ambientShadow: CALayer? { get set } diff --git a/Sources/FluentUI_iOS/Core/Theme/Tokens/TokenizedControl.swift b/Sources/FluentUI_iOS/Core/Theme/Tokens/TokenizedControl.swift index d132780a7d..868c9c34d3 100644 --- a/Sources/FluentUI_iOS/Core/Theme/Tokens/TokenizedControl.swift +++ b/Sources/FluentUI_iOS/Core/Theme/Tokens/TokenizedControl.swift @@ -14,9 +14,3 @@ public protocol TokenizedControl { /// The set of tokens associated with this `TokenizedControl`. var tokenSet: TokenSetType { get } } - -/// Internal extension to `TokenizedControl` that adds the ability to modify the active tokens. -protocol TokenizedControlInternal: TokenizedControl { - /// The current `FluentTheme` applied to this control. Usually acquired via the environment. - var fluentTheme: FluentTheme { get } -} diff --git a/Sources/FluentUI_iOS/Core/Theme/Tokens/TokenizedControlView.swift b/Sources/FluentUI_iOS/Core/Theme/Tokens/TokenizedControlView.swift index b31817519d..41f46c0b63 100644 --- a/Sources/FluentUI_iOS/Core/Theme/Tokens/TokenizedControlView.swift +++ b/Sources/FluentUI_iOS/Core/Theme/Tokens/TokenizedControlView.swift @@ -6,7 +6,7 @@ import SwiftUI /// SwiftUI-specific extension to `TokenizedControl`. -public protocol TokenizedControlOverridable: TokenizedControl { +public protocol TokenizedControlView: TokenizedControl { /// Modifier function that updates the design tokens for a given control. /// /// - Parameter tokens: The tokens to apply to this control. @@ -15,11 +15,8 @@ public protocol TokenizedControlOverridable: TokenizedControl { func overrideTokens(_ overrideTokens: [TokenSetKeyType: ControlTokenValue]?) -> Self } -/// Internal union of `TokenizedControlOverridable` and `TokenizedControlInternal` protocols. -internal protocol TokenizedControlView: TokenizedControlOverridable, TokenizedControlInternal {} - /// Common base type alias for all `state` objects. -typealias ControlState = NSObject & ObservableObject & Identifiable +public typealias ControlState = NSObject & ObservableObject & Identifiable // MARK: - Extensions