diff --git a/VDS.xcodeproj/project.pbxproj b/VDS.xcodeproj/project.pbxproj index a22c51b8..26d6c357 100644 --- a/VDS.xcodeproj/project.pbxproj +++ b/VDS.xcodeproj/project.pbxproj @@ -136,6 +136,7 @@ EAD068922A560B65002E3A2D /* LoaderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD068912A560B65002E3A2D /* LoaderViewController.swift */; }; EAD068942A560C13002E3A2D /* LoaderLaunchable.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */; }; EAD8D2C128BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */; }; + EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */ = {isa = PBXBuildFile; fileRef = EAE785302BA0A438009428EA /* UIImage+Helper.swift */; }; EAEEEC922B1F807300531FC2 /* BadgeChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */; }; EAEEEC962B1F893B00531FC2 /* ButtonChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */; }; EAEEEC982B1F8DD100531FC2 /* LineChangeLog.txt in Resources */ = {isa = PBXBuildFile; fileRef = EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */; }; @@ -311,6 +312,7 @@ EAD068912A560B65002E3A2D /* LoaderViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderViewController.swift; sourceTree = ""; }; EAD068932A560C13002E3A2D /* LoaderLaunchable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoaderLaunchable.swift; sourceTree = ""; }; EAD8D2C028BFDE8B006EB6A6 /* UIGestureRecognizer+Publisher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIGestureRecognizer+Publisher.swift"; sourceTree = ""; }; + EAE785302BA0A438009428EA /* UIImage+Helper.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIImage+Helper.swift"; sourceTree = ""; }; EAEEEC912B1F807300531FC2 /* BadgeChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = BadgeChangeLog.txt; sourceTree = ""; }; EAEEEC952B1F893B00531FC2 /* ButtonChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = ButtonChangeLog.txt; sourceTree = ""; }; EAEEEC972B1F8DD100531FC2 /* LineChangeLog.txt */ = {isa = PBXFileReference; lastKnownFileType = text; path = LineChangeLog.txt; sourceTree = ""; }; @@ -544,6 +546,7 @@ EA81410F2A127066004F60D2 /* UIColor+VDSColor.swift */, EA33623D2892EE950071C351 /* UIDevice.swift */, EA0B18092AA78F9000F2D0CD /* UIEdgeInsets.swift */, + EAE785302BA0A438009428EA /* UIImage+Helper.swift */, EAF7F0B6289C12A600B287F5 /* UITapGestureRecognizer.swift */, EA8E40902A7D3F6300934ED3 /* UIView+Accessibility.swift */, EAB5FED329267EB300998C17 /* UIView+NSLayoutConstraint.swift */, @@ -1040,6 +1043,7 @@ EA0D1C412A6AD61C00E5C127 /* Typography+Additional.swift in Sources */, EAC925842911C63100091998 /* Colorable.swift in Sources */, EAACB89A2B927108006A3869 /* Valuing.swift in Sources */, + EAE785312BA0A438009428EA /* UIImage+Helper.swift in Sources */, EAB5FEF5292D371F00998C17 /* ButtonBase.swift in Sources */, EA978EC5291D6AFE00ACC883 /* AnyLabelAttribute.swift in Sources */, EAC71A1F2A2E173D00E47A9F /* RadioButton.swift in Sources */, diff --git a/VDS/BaseClasses/Selector/SelectorItemBase.swift b/VDS/BaseClasses/Selector/SelectorItemBase.swift index 9ca86d8c..869c40e0 100644 --- a/VDS/BaseClasses/Selector/SelectorItemBase.swift +++ b/VDS/BaseClasses/Selector/SelectorItemBase.swift @@ -102,7 +102,6 @@ open class SelectorItemBase: Control, Errorable, /// Instead of use labelText and labelTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes. open var labelAttributedText: NSAttributedString? { didSet { - label.useAttributedText = !(labelAttributedText?.string.isEmpty ?? true) label.attributedText = labelAttributedText setNeedsUpdate() } @@ -117,7 +116,6 @@ open class SelectorItemBase: Control, Errorable, /// Instead of use childText and childTextAttirbutes, this is a fully baked NSAttributedString with both text and attributes. open var childAttributedText: NSAttributedString? { didSet { - childLabel.useAttributedText = !(childAttributedText?.string.isEmpty ?? true) childLabel.attributedText = childAttributedText setNeedsUpdate() } diff --git a/VDS/Classes/SelfSizingCollectionView.swift b/VDS/Classes/SelfSizingCollectionView.swift index c50082c4..ad5d5661 100644 --- a/VDS/Classes/SelfSizingCollectionView.swift +++ b/VDS/Classes/SelfSizingCollectionView.swift @@ -34,10 +34,17 @@ public final class SelfSizingCollectionView: UICollectionView { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- - private var contentSizeObservation: NSKeyValueObservation? private var collectionViewHeight: NSLayoutConstraint? private var anyCancellable: AnyCancellable? - + private var contentSizeSubject = CurrentValueSubject(.zero) + + //-------------------------------------------------- + // MARK: - Public Properties + //-------------------------------------------------- + public var contentSizePublisher: AnyPublisher { + contentSizeSubject.eraseToAnyPublisher() + } + //-------------------------------------------------- // MARK: - Overrides //-------------------------------------------------- @@ -45,7 +52,6 @@ public final class SelfSizingCollectionView: UICollectionView { /// The natural size for the receiving view, considering only properties of the view itself. public override var intrinsicContentSize: CGSize { let contentSize = self.contentSize - //print(#function, contentSize) return CGSize(width: UIView.noIntrinsicMetric, height: contentSize.height) } @@ -67,18 +73,17 @@ public final class SelfSizingCollectionView: UICollectionView { //ensure autoLayout uses intrinsic height setContentHuggingPriority(.required, for: .vertical) setContentCompressionResistancePriority(.required, for: .vertical) - collectionViewHeight = heightAnchor.constraint(equalToConstant: 0).activate() - - // Observing the value of contentSize seems to be the only reliable way to get the contentSize after the collection view lays out its subviews. - self.contentSizeObservation = self.observe(\.contentSize, options: [.old, .new]) { [weak self] _, change in - // If we don't specify `options: [.old, .new]`, the change.oldValue and .newValue will always be `nil`. - if change.newValue != change.oldValue { - self?.invalidateIntrinsicContentSize() - if let height = change.newValue?.height { - self?.collectionViewHeight?.constant = height + collectionViewHeight = height(constant: 0, priority: .defaultHigh) + + anyCancellable = self.publisher(for: \.contentSize, options: [.new]) + .sink { [weak self] compare in + guard let self else { return } + if compare.height != self.collectionViewHeight?.constant { + self.invalidateIntrinsicContentSize() + self.collectionViewHeight?.constant = compare.height + self.contentSizeSubject.send(compare) } } - } } } diff --git a/VDS/Components/Buttons/ButtonBase.swift b/VDS/Components/Buttons/ButtonBase.swift index 255c6f64..cc096d41 100644 --- a/VDS/Components/Buttons/ButtonBase.swift +++ b/VDS/Components/Buttons/ButtonBase.swift @@ -102,6 +102,7 @@ open class ButtonBase: UIButton, ViewProtocol, UserInfoable, Clickable { //-------------------------------------------------- open func initialSetup() { if !initialSetupPerformed { + initialSetupPerformed = true backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false accessibilityCustomActions = [] diff --git a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift index 7d506961..a51371c2 100644 --- a/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift +++ b/VDS/Components/Buttons/ButtonGroup/ButtonGroup.swift @@ -9,6 +9,7 @@ import Foundation import UIKit import VDSColorTokens import VDSFormControlsTokens +import Combine /// A button group contains combinations of related CTAs including ``Button``, ``TextLink``, and ``TextLinkCaret``. This group component controls a combination's orientation, spacing, size and allowable size pairings. @objc(VDSButtonGroup) @@ -98,6 +99,8 @@ open class ButtonGroup: View { buttons.forEach { $0.surface = surface } } } + + open var contentSizePublisher: AnyPublisher { collectionView.contentSizePublisher } //-------------------------------------------------- // MARK: - Private Properties @@ -108,6 +111,7 @@ open class ButtonGroup: View { $0.delegate = self } + /// CollectionView that renders the array of buttonBase obects. fileprivate lazy var collectionView: SelfSizingCollectionView = { return SelfSizingCollectionView(frame: .zero, collectionViewLayout: positionLayout).with { diff --git a/VDS/Components/Icon/Icon.swift b/VDS/Components/Icon/Icon.swift index 5ac143f6..f52279f1 100644 --- a/VDS/Components/Icon/Icon.swift +++ b/VDS/Components/Icon/Icon.swift @@ -44,7 +44,7 @@ open class Icon: View { //-------------------------------------------------- /// UIImageView used to render the icon. open var imageView = UIImageView().with { - $0.isAccessibilityElement = false + $0.isAccessibilityElement = false $0.translatesAutoresizingMaskIntoConstraints = false $0.contentMode = .scaleAspectFill $0.clipsToBounds = true @@ -109,8 +109,8 @@ open class Icon: View { //get the image name //set the image - if let name, let image = getImage(for: name.rawValue) { - setImage(image: image, imageColor: imageColor) + if let name, let image = UIImage.image(for: name, color: imageColor) { + imageView.image = image } else { imageView.image = nil } @@ -129,17 +129,17 @@ open class Icon: View { super.updateAccessibility() accessibilityLabel = name?.rawValue ?? "icon" } - - //-------------------------------------------------- - // MARK: - Private Methods - //-------------------------------------------------- - private func getImage(for imageName: String) -> UIImage? { - - return BundleManager.shared.image(for: imageName) - } - - private func setImage(image: UIImage, imageColor: UIColor) { - imageView.image = image.withTintColor(imageColor) - } } +extension UIImage { + + /// UIImage helper for finding images based on the Icon.Name which uses the internal BundleManager. + /// - Parameters: + /// - name: Icon.Name rawRepresentable. + /// - color: Color to Tint the image with + /// - renderingMode: UIImage Rendering mode. + /// - Returns: UIImage for this proecess + public static func image(for iconName: Icon.Name, color: UIColor? = nil, renderingMode: UIImage.RenderingMode = .alwaysOriginal) -> UIImage? { + image(representing: iconName, color: color, renderingMode: renderingMode) + } +} diff --git a/VDS/Components/Label/Label.swift b/VDS/Components/Label/Label.swift index 67e53abe..955d9604 100644 --- a/VDS/Components/Label/Label.swift +++ b/VDS/Components/Label/Label.swift @@ -42,6 +42,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- // MARK: - Private Properties //-------------------------------------------------- + private enum TextSetMode { + case text + case attributedText + } + + private var textSetMode: TextSetMode = .text + private var initialSetupPerformed = false private var edgeInsets: UIEdgeInsets { textStyle.edgeInsets } @@ -101,18 +108,14 @@ open class Label: UILabel, ViewProtocol, UserInfoable { // MARK: - Public Properties //-------------------------------------------------- /// Key of whether or not updateView() is called in setNeedsUpdate() - open var shouldUpdateView: Bool = true - - /// Determines if the label should use its own attributedText property instead of rendering the attributedText propert - /// based of other local properties, such as textStyle, textColor, surface, etc... The default value is false. - open var useAttributedText: Bool = false + open var shouldUpdateView: Bool = true /// Will determine if a scaled font should be used for the font. open var useScaledFont: Bool = false { didSet { setNeedsUpdate() }} open var surface: Surface = .light { didSet { setNeedsUpdate() }} - /// Array of LabelAttributeModel objects used in rendering the text. + /// Array of LabelAttributeModel objects used in rendering the text. open var attributes: [any LabelAttributeModel]? { didSet { setNeedsUpdate() }} /// TextStyle used on the this label. @@ -128,19 +131,40 @@ open class Label: UILabel, ViewProtocol, UserInfoable { /// Line break mode for the label, default is set to word wrapping. open override var lineBreakMode: NSLineBreakMode { didSet { setNeedsUpdate() }} - - private var _text: String? - + /// Text that will be used in the label. - override open var text: String? { - get { _text } - set { - if _text != newValue || newValue != attributedText?.string { - _text = newValue - useAttributedText = false - attributes?.removeAll() - setNeedsUpdate() + private var _text: String! + override open var text: String! { + didSet { + _text = text + textSetMode = .text + setNeedsUpdate() + } + } + + ///AttributedText that will be used in the label. + override open var attributedText: NSAttributedString? { + didSet { + textSetMode = .attributedText + setNeedsUpdate() + } + } + + override open var font: UIFont! { + didSet { + if let font, initialSetupPerformed { + textStyle = TextStyle.convert(font: font) } + setNeedsUpdate() + } + } + + override open var textColor: UIColor! { + didSet { + if let textColor, initialSetupPerformed { + textColorConfiguration = SurfaceColorConfiguration(textColor, textColor).eraseToAnyColorable() + } + setNeedsUpdate() } } @@ -162,13 +186,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- open func initialSetup() { if !initialSetupPerformed { + initialSetupPerformed = true //register for ContentSizeChanges NotificationCenter .Publisher(center: .default, name: UIContentSizeCategory.didChangeNotification) .sink { [weak self] notification in self?.setNeedsUpdate() }.store(in: &subscribers) - backgroundColor = .clear numberOfLines = 0 lineBreakMode = .byWordWrapping @@ -200,30 +224,13 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } open func updateView() { - if !useAttributedText { - if let text { - accessibilityCustomActions = [] - - //create the primary string - let mutableText = NSMutableAttributedString.mutableText(for: text, - textStyle: textStyle, - useScaledFont: useScaledFont, - textColor: textColorConfiguration.getColor(self), - alignment: textAlignment, - lineBreakMode: lineBreakMode) - - applyAttributes(mutableText) - - //set the attributed text - attributedText = mutableText - - //force a drawText - setNeedsDisplay() - - setNeedsLayout() - layoutIfNeeded() - } - } + restyleText() + + //force a drawText + setNeedsDisplay() + + setNeedsLayout() + layoutIfNeeded() } open func updateAccessibility() { @@ -269,6 +276,55 @@ open class Label: UILabel, ViewProtocol, UserInfoable { //-------------------------------------------------- // MARK: - Private Methods //-------------------------------------------------- + private func restyleText() { + if textSetMode == .text { + styleText(_text) + } else { + styleAttributedText(attributedText) + } + } + + private func styleText(_ newValue: String!) { + defer { invalidateIntrinsicContentSize() } + guard let newValue, !newValue.isEmpty else { + // We don't need to use attributed text + super.attributedText = nil + super.text = newValue + return + } + + accessibilityCustomActions = [] + + //create the primary string + let mutableText = NSMutableAttributedString.mutableText(for: newValue, + textStyle: textStyle, + useScaledFont: useScaledFont, + textColor: textColorConfiguration.getColor(self), + alignment: textAlignment, + lineBreakMode: lineBreakMode) + + applyAttributes(mutableText) + + // Set attributed text to match typography + super.attributedText = mutableText + } + + private func styleAttributedText(_ newValue: NSAttributedString?) { + defer { invalidateIntrinsicContentSize() } + guard let newValue, !newValue.string.isEmpty else { + // We don't need any additional styling + super.attributedText = newValue + return + } + + let mutableText = NSMutableAttributedString(attributedString: newValue) + + applyAttributes(mutableText) + + // Modify attributed text to match typography + super.attributedText = mutableText + } + private func applyAttributes(_ mutableAttributedString: NSMutableAttributedString) { actions = [] @@ -366,3 +422,5 @@ open class Label: UILabel, ViewProtocol, UserInfoable { } } } + + diff --git a/VDS/Components/RadioBox/RadioBoxItem.swift b/VDS/Components/RadioBox/RadioBoxItem.swift index 81f3f709..cfeb54e6 100644 --- a/VDS/Components/RadioBox/RadioBoxItem.swift +++ b/VDS/Components/RadioBox/RadioBoxItem.swift @@ -100,7 +100,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable { /// If provided, the RadioBox textAttributedText will be rendered. open var textAttributedText: NSAttributedString? { didSet { - textLabel.useAttributedText = !(textAttributedText?.string.isEmpty ?? true) textLabel.attributedText = textAttributedText setNeedsUpdate() } @@ -115,7 +114,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable { /// If provided, the RadioBox subTextAttributedText will be rendered. open var subTextAttributedText: NSAttributedString? { didSet { - subTextLabel.useAttributedText = !(subTextAttributedText?.string.isEmpty ?? true) subTextLabel.attributedText = subTextAttributedText setNeedsUpdate() } @@ -130,7 +128,6 @@ open class RadioBoxItem: Control, Changeable, FormFieldable { /// If provided, the RadioBox subTextRightAttributedText will be rendered. open var subTextRightAttributedText: NSAttributedString? { didSet { - subTextRightLabel.useAttributedText = !(subTextRightAttributedText?.string.isEmpty ?? true) subTextRightLabel.attributedText = subTextRightAttributedText setNeedsUpdate() } diff --git a/VDS/Components/TextFields/TextArea/TextView.swift b/VDS/Components/TextFields/TextArea/TextView.swift index ea96ed6e..ffb355d1 100644 --- a/VDS/Components/TextFields/TextArea/TextView.swift +++ b/VDS/Components/TextFields/TextArea/TextView.swift @@ -94,6 +94,7 @@ open class TextView: UITextView, ViewProtocol { //-------------------------------------------------- open func initialSetup() { if !initialSetupPerformed { + initialSetupPerformed = true backgroundColor = .clear translatesAutoresizingMaskIntoConstraints = false accessibilityCustomActions = [] diff --git a/VDS/Components/TitleLockup/TitleLockup.swift b/VDS/Components/TitleLockup/TitleLockup.swift index 7feb86e7..761ac9c5 100644 --- a/VDS/Components/TitleLockup/TitleLockup.swift +++ b/VDS/Components/TitleLockup/TitleLockup.swift @@ -68,6 +68,7 @@ open class TitleLockup: View { /// Label used to render the eyebrow model. open var eyebrowLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentHuggingPriority(.required, for: .vertical) } /// Model used in rendering the eyebrow label. @@ -77,6 +78,7 @@ open class TitleLockup: View { /// Label used to render the title model. open var titleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentHuggingPriority(.required, for: .vertical) $0.accessibilityTraits.insert([.header]) } @@ -87,6 +89,7 @@ open class TitleLockup: View { /// Label used to render the subtitle model. open var subTitleLabel = Label().with { $0.setContentCompressionResistancePriority(.required, for: .vertical) + $0.setContentHuggingPriority(.required, for: .vertical) } /// Model used in rendering the subtitle label. @@ -380,7 +383,7 @@ open class TitleLockup: View { } //pin the last view to the bottom of this view - previousView?.pinBottom(0, .defaultHigh) + previousView?.pinBottom(0) //debugging for borders eyebrowLabel.debugBorder(show: hasDebugBorder, color: .green) diff --git a/VDS/Extensions/UIImage+Helper.swift b/VDS/Extensions/UIImage+Helper.swift new file mode 100644 index 00000000..c8f103dc --- /dev/null +++ b/VDS/Extensions/UIImage+Helper.swift @@ -0,0 +1,26 @@ +// +// UIImage+Icon.swift +// VDS +// +// Created by Matt Bruce on 3/11/24. +// + +import Foundation +import UIKit + +extension UIImage { + + /// UIImage helper for finding images based on the Icon.Name which uses the internal BundleManager. + /// - Parameters: + /// - name: RawRepresentable. + /// - color: Color to Tint the image with + /// - renderingMode: UIImage Rendering mode. + /// - Returns: UIImage for this proecess + public static func image(representing representable: T, color: UIColor? = nil, renderingMode: UIImage.RenderingMode = .alwaysOriginal) -> UIImage? where T.RawValue == String { + guard let image = BundleManager.shared.image(for: representable.rawValue) else { return nil } + + guard let color else { return image } + + return image.withTintColor(color, renderingMode: renderingMode) + } +} diff --git a/VDS/Fonts/Font.swift b/VDS/Fonts/Font.swift index c5caaa32..f4ba798d 100644 --- a/VDS/Fonts/Font.swift +++ b/VDS/Fonts/Font.swift @@ -6,15 +6,17 @@ // import Foundation +import UIKit /// Enum that is matched up for the Verizon fonts. -public enum Font: String, FontProtocol { +public enum Font: FontProtocol { case edsBold case edsRegular case dsLight case etxBold case etxRegular - + case custom(UIFont) + public var fontName: String { switch self { case .edsBold: @@ -27,11 +29,32 @@ public enum Font: String, FontProtocol { return "VerizonNHGeTX-Bold" case .etxRegular: return "VerizonNHGeTX-Regular" + case .custom(let font): + return font.fontName } } + public static var allCases: [Font] { + [.edsBold, .edsRegular, .dsLight, .etxBold, .etxRegular] + } + /// File Extension for each of the Font enums. public var fontFileExtension: String { return "otf" } + + /// Returns a UIFont for the fontName and size given. + /// - Parameters: + /// - size: Size of the font + /// - Returns: UIFont for the fontName and Size. + public func font(ofSize size: CGFloat) -> UIFont{ + DispatchQueue.once(block: { self.register() }) + switch self { + case .custom(let font): + return font + default: + guard let found = UIFont(name: self.fontName, size: size) else { return .systemFont(ofSize: size) } + return found + } + } } diff --git a/VDS/Fonts/FontProtocol.swift b/VDS/Fonts/FontProtocol.swift index 9ee21939..df6ac6a3 100644 --- a/VDS/Fonts/FontProtocol.swift +++ b/VDS/Fonts/FontProtocol.swift @@ -9,9 +9,10 @@ import Foundation import UIKit /// Used in Classes that require Fonts -public protocol FontProtocol: CaseIterable, RawRepresentable, Hashable { +public protocol FontProtocol { var fontFileExtension: String { get } var fontName: String { get } + static var allCases: [Self] { get } } extension FontProtocol { diff --git a/VDS/Typography/Typogprahy+Styles.swift b/VDS/Typography/Typogprahy+Styles.swift index ec144617..0136d3c4 100644 --- a/VDS/Typography/Typogprahy+Styles.swift +++ b/VDS/Typography/Typogprahy+Styles.swift @@ -209,6 +209,13 @@ extension TextStyle { boldMicro ] } + + public static func convert(font: UIFont) -> TextStyle { + guard let found = allCases.first(where: { font.fontName == $0.fontFace.fontName && font.pointSize == $0.pointSize} ) else { + return TextStyle(rawValue: "Custom\(font.fontName)", fontFace: .custom(font), pointSize: font.pointSize) + } + return found + } } extension TextStyle {