From 8a2d1f5f6a05a0952692e1e3a6b8f6a7c325ee59 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Sun, 15 Sep 2019 23:02:13 -0700 Subject: [PATCH 1/5] Create TimelineAvatarView, which draws the background for images that need it. --- .../Timeline/Cell/TimelineAvatarView.swift | 118 ++++++++++++++++++ NetNewsWire.xcodeproj/project.pbxproj | 16 ++- Shared/Extensions/RSImage-Extensions.swift | 6 +- 3 files changed, 131 insertions(+), 9 deletions(-) create mode 100644 Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift diff --git a/Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift b/Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift new file mode 100644 index 000000000..f6f5192ef --- /dev/null +++ b/Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift @@ -0,0 +1,118 @@ +// +// TimelineAvatarView.swift +// NetNewsWire +// +// Created by Brent Simmons on 9/15/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import AppKit + +final class TimelineAvatarView: NSView { + + var image: NSImage? = nil { + didSet { + imageView.image = image + updateHasExposedBackground() + needsDisplay = true + needsLayout = true + } + } + + override var isFlipped: Bool { + return true + } + + private let imageView: NSImageView = { + let imageView = NSImageView(frame: NSRect.zero) + imageView.animates = false + imageView.imageAlignment = .alignCenter + imageView.imageScaling = .scaleProportionallyDown + return imageView + }() + + private var hasExposedBackground = true { + didSet { + if oldValue != hasExposedBackground { + needsDisplay = true + } + } + } + + override init(frame frameRect: NSRect) { + super.init(frame: frameRect) + commonInit() + } + + required init?(coder: NSCoder) { + super.init(coder: coder) + commonInit() + } + + convenience init() { + self.init(frame: NSRect.zero) + } + + override func viewDidMoveToSuperview() { + needsLayout = true + needsDisplay = true + } + + override func layout() { + resizeSubviews(withOldSize: NSZeroSize) + } + + override func resizeSubviews(withOldSize oldSize: NSSize) { + imageView.rs_setFrameIfNotEqual(rectForImageView()) + } + + override func draw(_ dirtyRect: NSRect) { + guard hasExposedBackground else { + return + } + let rImage = imageView.frame + if rImage.contains(dirtyRect) { + return + } + + let color = NSApplication.shared.effectiveAppearance.isDarkMode ? AppAssets.avatarDarkBackgroundColor : AppAssets.avatarLightBackgroundColor + color.set() + dirtyRect.fill() + } +} + +private extension TimelineAvatarView { + + func commonInit() { + addSubview(imageView) + } + + func rectForImageView() -> NSRect { + guard let image = image else { + return NSRect.zero + } + + let imageSize = image.size + let viewSize = bounds.size + if imageSize.height == imageSize.width { + return NSMakeRect(0.0, 0.0, viewSize.width, viewSize.height) + } + else if imageSize.height > imageSize.width { + let factor = viewSize.height / imageSize.height + let width = imageSize.width * factor + let originX = floor((viewSize.width - width) / 2.0) + return NSMakeRect(originX, 0.0, width, viewSize.height) + } + + // Wider than tall: imageSize.width > imageSize.height + let factor = viewSize.width / imageSize.width + let height = imageSize.height * factor + let originY = floor((viewSize.height - height) / 2.0) + return NSMakeRect(0.0, originY, viewSize.width, height) + } + + func updateHasExposedBackground() { + let rImage = rectForImageView() + hasExposedBackground = rImage.size.height < bounds.size.height || rImage.size.width < bounds.size.width + } +} diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index c13da6839..64b281494 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -187,6 +187,7 @@ 84702AA41FA27AC0006B8943 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8472058020142E8900AD578B /* FeedInspectorViewController.swift */; }; 8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; }; + 847CD6CA232F4CBF00FAC46D /* TimelineAvatarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* TimelineAvatarView.swift */; }; 847E64A02262783000E00365 /* NSAppleEventDescriptor+UserRecordFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */; }; 848362FD2262A30800DA1D35 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 848362FC2262A30800DA1D35 /* styleSheet.css */; }; 848362FF2262A30E00DA1D35 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; }; @@ -797,6 +798,7 @@ 8472058020142E8900AD578B /* FeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedInspectorViewController.swift; sourceTree = ""; }; 847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; }; 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedDelegate.swift; sourceTree = ""; }; + 847CD6C9232F4CBF00FAC46D /* TimelineAvatarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineAvatarView.swift; sourceTree = ""; }; 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAppleEventDescriptor+UserRecordFields.swift"; sourceTree = ""; }; 848362FC2262A30800DA1D35 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; 848362FE2262A30E00DA1D35 /* template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = template.html; sourceTree = ""; }; @@ -1422,6 +1424,7 @@ 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */, 849A97711ED9EC04007D329B /* TimelineCellData.swift */, 849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */, + 847CD6C9232F4CBF00FAC46D /* TimelineAvatarView.swift */, ); path = Cell; sourceTree = ""; @@ -1960,12 +1963,12 @@ ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { 6581C73220CED60000F4AD34 = { - DevelopmentTeam = SHJK2V3AJG; - ProvisioningStyle = Automatic; + DevelopmentTeam = M8L2WTLA8W; + ProvisioningStyle = Manual; }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = M8L2WTLA8W; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { @@ -1981,8 +1984,8 @@ }; 849C645F1ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = SHJK2V3AJG; - ProvisioningStyle = Automatic; + DevelopmentTeam = M8L2WTLA8W; + ProvisioningStyle = Manual; SystemCapabilities = { com.apple.HardenedRuntime = { enabled = 1; @@ -1991,7 +1994,7 @@ }; 849C64701ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = SHJK2V3AJG; + DevelopmentTeam = 9C84TZ7Q6Z; ProvisioningStyle = Automatic; TestTargetID = 849C645F1ED37A5D003D8FC0; }; @@ -2430,6 +2433,7 @@ files = ( 84F204E01FAACBB30076E152 /* ArticleArray.swift in Sources */, 848B937221C8C5540038DC0D /* CrashReporter.swift in Sources */, + 847CD6CA232F4CBF00FAC46D /* TimelineAvatarView.swift in Sources */, 84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */, 51EF0F7A22771B890050506E /* ColorHash.swift in Sources */, 84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */, diff --git a/Shared/Extensions/RSImage-Extensions.swift b/Shared/Extensions/RSImage-Extensions.swift index 3406a959b..0b566b632 100644 --- a/Shared/Extensions/RSImage-Extensions.swift +++ b/Shared/Extensions/RSImage-Extensions.swift @@ -28,9 +28,9 @@ extension RSImage { return nil } - if cgImage.width < avatarSize || cgImage.height < avatarSize { - cgImage = RSImage.compositeAvatar(cgImage) - } +// if cgImage.width < avatarSize || cgImage.height < avatarSize { +// cgImage = RSImage.compositeAvatar(cgImage) +// } #if os(iOS) return RSImage(cgImage: cgImage) From 2f9f4e263c567d2407fbd8d958a528d2dbe75e48 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 16 Sep 2019 19:59:33 -0700 Subject: [PATCH 2/5] Restore color for light avatar background. --- .../avatarLightBackgroundColor.colorset/Contents.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Mac/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json b/Mac/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json index 57a5cc49d..af234f435 100644 --- a/Mac/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json +++ b/Mac/Resources/Assets.xcassets/avatarLightBackgroundColor.colorset/Contents.json @@ -17,4 +17,4 @@ } } ] -} \ No newline at end of file +} From 506b621e5dd3c43478640ff17b103a2f3b90a963 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 16 Sep 2019 20:00:32 -0700 Subject: [PATCH 3/5] Draw the avatar background in TimelineAvatarView only if the icon is too short vertically. --- .../Timeline/Cell/TimelineAvatarView.swift | 43 +++++++++---------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift b/Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift index f6f5192ef..e84ec4a08 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineAvatarView.swift @@ -12,10 +12,11 @@ final class TimelineAvatarView: NSView { var image: NSImage? = nil { didSet { - imageView.image = image - updateHasExposedBackground() - needsDisplay = true - needsLayout = true + if image !== oldValue { + imageView.image = image + needsDisplay = true + needsLayout = true + } } } @@ -27,18 +28,17 @@ final class TimelineAvatarView: NSView { let imageView = NSImageView(frame: NSRect.zero) imageView.animates = false imageView.imageAlignment = .alignCenter - imageView.imageScaling = .scaleProportionallyDown + imageView.imageScaling = .scaleProportionallyUpOrDown return imageView }() - private var hasExposedBackground = true { - didSet { - if oldValue != hasExposedBackground { - needsDisplay = true - } - } + private var hasExposedVerticalBackground: Bool { + return imageView.frame.size.height < bounds.size.height } + private static var lightBackgroundColor = AppAssets.avatarLightBackgroundColor + private static var darkBackgroundColor = AppAssets.avatarDarkBackgroundColor + override init(frame frameRect: NSRect) { super.init(frame: frameRect) commonInit() @@ -67,15 +67,11 @@ final class TimelineAvatarView: NSView { } override func draw(_ dirtyRect: NSRect) { - guard hasExposedBackground else { - return - } - let rImage = imageView.frame - if rImage.contains(dirtyRect) { + guard hasExposedVerticalBackground else { return } - let color = NSApplication.shared.effectiveAppearance.isDarkMode ? AppAssets.avatarDarkBackgroundColor : AppAssets.avatarLightBackgroundColor + let color = NSApplication.shared.effectiveAppearance.isDarkMode ? TimelineAvatarView.darkBackgroundColor : TimelineAvatarView.lightBackgroundColor color.set() dirtyRect.fill() } @@ -85,6 +81,7 @@ private extension TimelineAvatarView { func commonInit() { addSubview(imageView) + wantsLayer = true } func rectForImageView() -> NSRect { @@ -95,7 +92,12 @@ private extension TimelineAvatarView { let imageSize = image.size let viewSize = bounds.size if imageSize.height == imageSize.width { - return NSMakeRect(0.0, 0.0, viewSize.width, viewSize.height) + if imageSize.height >= viewSize.height * 0.75 { + // Close enough to viewSize to scale up the image. + return NSMakeRect(0.0, 0.0, viewSize.width, viewSize.height) + } + let offset = floor((viewSize.height - imageSize.height) / 2.0) + return NSMakeRect(offset, offset, imageSize.width, imageSize.height) } else if imageSize.height > imageSize.width { let factor = viewSize.height / imageSize.height @@ -110,9 +112,4 @@ private extension TimelineAvatarView { let originY = floor((viewSize.height - height) / 2.0) return NSMakeRect(0.0, originY, viewSize.width, height) } - - func updateHasExposedBackground() { - let rImage = rectForImageView() - hasExposedBackground = rImage.size.height < bounds.size.height || rImage.size.width < bounds.size.width - } } From a225d52ba3bb03419fb0bed3b95edf97628c5072 Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 16 Sep 2019 20:01:30 -0700 Subject: [PATCH 4/5] Use the new TimelineAvatarView instead of an NSImageView in TimelineTableCellView. --- .../Timeline/Cell/TimelineTableCellView.swift | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift index 680b5e2db..22a1628dd 100644 --- a/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift +++ b/Mac/MainWindow/Timeline/Cell/TimelineTableCellView.swift @@ -18,13 +18,7 @@ class TimelineTableCellView: NSTableCellView { private let dateView = TimelineTableCellView.singleLineTextField() private let feedNameView = TimelineTableCellView.singleLineTextField() - private lazy var avatarImageView: NSImageView = { - let imageView = TimelineTableCellView.imageView(with: AppAssets.genericFeedImage, scaling: .scaleNone) - imageView.imageAlignment = .alignTop - imageView.imageScaling = .scaleProportionallyDown - imageView.wantsLayer = true - return imageView - }() + private lazy var avatarView = TimelineAvatarView() private let starView = TimelineTableCellView.imageView(with: AppAssets.timelineStar, scaling: .scaleNone) private let separatorView = TimelineTableCellView.separatorView() @@ -43,7 +37,7 @@ class TimelineTableCellView: NSTableCellView { didSet { if cellAppearance != oldValue { updateTextFieldFonts() - avatarImageView.layer?.cornerRadius = cellAppearance.avatarCornerRadius + avatarView.layer?.cornerRadius = cellAppearance.avatarCornerRadius needsLayout = true } } @@ -125,7 +119,7 @@ class TimelineTableCellView: NSTableCellView { dateView.rs_setFrameIfNotEqual(layoutRects.dateRect) unreadIndicatorView.rs_setFrameIfNotEqual(layoutRects.unreadIndicatorRect) feedNameView.rs_setFrameIfNotEqual(layoutRects.feedNameRect) - avatarImageView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect) + avatarView.rs_setFrameIfNotEqual(layoutRects.avatarImageRect) starView.rs_setFrameIfNotEqual(layoutRects.starRect) separatorView.rs_setFrameIfNotEqual(layoutRects.separatorRect) } @@ -213,7 +207,7 @@ private extension TimelineTableCellView { addSubviewAtInit(unreadIndicatorView, hidden: true) addSubviewAtInit(dateView, hidden: false) addSubviewAtInit(feedNameView, hidden: true) - addSubviewAtInit(avatarImageView, hidden: true) + addSubviewAtInit(avatarView, hidden: true) addSubviewAtInit(starView, hidden: true) addSubviewAtInit(separatorView, hidden: !AppDefaults.timelineShowsSeparators) @@ -222,7 +216,7 @@ private extension TimelineTableCellView { func updatedLayoutRects() -> TimelineCellLayout { - return TimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, appearance: cellAppearance, hasAvatar: avatarImageView.image != nil) + return TimelineCellLayout(width: bounds.width, height: bounds.height, cellData: cellData, appearance: cellAppearance, hasAvatar: avatarView.image != nil) } func updateTitleView() { @@ -277,19 +271,19 @@ private extension TimelineTableCellView { return } - showView(avatarImageView) - if avatarImageView.image !== image { - avatarImageView.image = image + showView(avatarView) + if avatarView.image !== image { + avatarView.image = image needsLayout = true } } func makeAvatarEmpty() { - if avatarImageView.image != nil { - avatarImageView.image = nil + if avatarView.image != nil { + avatarView.image = nil needsLayout = true } - hideView(avatarImageView) + hideView(avatarView) } func hideView(_ view: NSView) { From 2d22f061a5f0eda49a79bf1547671a06a6c3d21b Mon Sep 17 00:00:00 2001 From: Brent Simmons Date: Mon, 16 Sep 2019 20:07:07 -0700 Subject: [PATCH 5/5] Remove no-longer-needed functions for compositing the avatar on a background. --- Shared/Extensions/RSImage-Extensions.swift | 55 +--------------------- 1 file changed, 2 insertions(+), 53 deletions(-) diff --git a/Shared/Extensions/RSImage-Extensions.swift b/Shared/Extensions/RSImage-Extensions.swift index 0b566b632..a09cafeb7 100644 --- a/Shared/Extensions/RSImage-Extensions.swift +++ b/Shared/Extensions/RSImage-Extensions.swift @@ -27,64 +27,13 @@ extension RSImage { guard var cgImage = RSImage.scaleImage(data, maxPixelSize: scaledMaxPixelSize) else { return nil } - -// if cgImage.width < avatarSize || cgImage.height < avatarSize { -// cgImage = RSImage.compositeAvatar(cgImage) -// } - + #if os(iOS) return RSImage(cgImage: cgImage) #else let size = NSSize(width: cgImage.width, height: cgImage.height) return RSImage(cgImage: cgImage, size: size) - #endif - + #endif } - } -private extension RSImage { - - #if os(iOS) - - static func compositeAvatar(_ avatar: CGImage) -> CGImage { - let rect = CGRect(x: 0, y: 0, width: avatarSize, height: avatarSize) - UIGraphicsBeginImageContext(rect.size) - if let context = UIGraphicsGetCurrentContext() { - context.setFillColor(AppAssets.avatarLightBackgroundColor.cgColor) - context.fill(rect) - context.translateBy(x: 0.0, y: CGFloat(integerLiteral: avatarSize)); - context.scaleBy(x: 1.0, y: -1.0) - let avatarRect = CGRect(x: (avatarSize - avatar.width) / 2, y: (avatarSize - avatar.height) / 2, width: avatar.width, height: avatar.height) - context.draw(avatar, in: avatarRect) - } - let img = UIGraphicsGetImageFromCurrentImageContext() - UIGraphicsEndImageContext() - return img!.cgImage! - } - - #else - - static func compositeAvatar(_ avatar: CGImage) -> CGImage { - var resultRect = CGRect(x: 0, y: 0, width: avatarSize, height: avatarSize) - let resultImage = NSImage(size: resultRect.size) - - resultImage.lockFocus() - if let context = NSGraphicsContext.current?.cgContext { - if NSApplication.shared.effectiveAppearance.isDarkMode { - context.setFillColor(AppAssets.avatarDarkBackgroundColor.cgColor) - } else { - context.setFillColor(AppAssets.avatarLightBackgroundColor.cgColor) - } - context.fill(resultRect) - let avatarRect = CGRect(x: (avatarSize - avatar.width) / 2, y: (avatarSize - avatar.height) / 2, width: avatar.width, height: avatar.height) - context.draw(avatar, in: avatarRect) - } - resultImage.unlockFocus() - - return resultImage.cgImage(forProposedRect: &resultRect, context: nil, hints: nil)! - } - - #endif - -}