Create and use MultiLineTextFieldSizer for sizing the title/text field in the timeline.
This commit is contained in:
parent
6d46b44e22
commit
127dd24016
@ -141,6 +141,7 @@
|
||||
84DAEE301F86CAFE0058304B /* OPMLImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */; };
|
||||
84DAEE321F870B390058304B /* DockBadge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DAEE311F870B390058304B /* DockBadge.swift */; };
|
||||
84E185B3203B74E500F69BFA /* SingleLineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */; };
|
||||
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */; };
|
||||
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */; };
|
||||
84E850861FCB60CE0072EA88 /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; };
|
||||
84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */; };
|
||||
@ -663,6 +664,7 @@
|
||||
84DAEE2F1F86CAFE0058304B /* OPMLImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OPMLImporter.swift; sourceTree = "<group>"; };
|
||||
84DAEE311F870B390058304B /* DockBadge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = DockBadge.swift; path = Evergreen/DockBadge.swift; sourceTree = "<group>"; };
|
||||
84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SingleLineTextFieldSizer.swift; sourceTree = "<group>"; };
|
||||
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MultilineTextFieldSizer.swift; sourceTree = "<group>"; };
|
||||
84E46C7C1F75EF7B005ECFB3 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AppDefaults.swift; path = Evergreen/AppDefaults.swift; sourceTree = "<group>"; };
|
||||
84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorAvatarDownloader.swift; sourceTree = "<group>"; };
|
||||
84E8E0DA202EC49300562D8F /* TimelineViewController+ContextualMenus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TimelineViewController+ContextualMenus.swift"; sourceTree = "<group>"; };
|
||||
@ -1011,6 +1013,7 @@
|
||||
849A97701ED9EC04007D329B /* TimelineCellAppearance.swift */,
|
||||
849A97721ED9EC04007D329B /* TimelineCellLayout.swift */,
|
||||
84E185B2203B74E500F69BFA /* SingleLineTextFieldSizer.swift */,
|
||||
84E185C2203BB12600F69BFA /* MultilineTextFieldSizer.swift */,
|
||||
849A97711ED9EC04007D329B /* TimelineCellData.swift */,
|
||||
849A97731ED9EC04007D329B /* TimelineStringUtilities.swift */,
|
||||
849A97751ED9EC04007D329B /* UnreadIndicatorView.swift */,
|
||||
@ -1952,6 +1955,7 @@
|
||||
849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */,
|
||||
84E8E0DB202EC49300562D8F /* TimelineViewController+ContextualMenus.swift in Sources */,
|
||||
849A97791ED9EC04007D329B /* TimelineStringUtilities.swift in Sources */,
|
||||
84E185C3203BB12600F69BFA /* MultilineTextFieldSizer.swift in Sources */,
|
||||
843A3B5620311E7700BF76EC /* FeedListOutlineView.swift in Sources */,
|
||||
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
||||
84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */,
|
||||
|
128
Evergreen/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift
Normal file
128
Evergreen/MainWindow/Timeline/Cell/MultilineTextFieldSizer.swift
Normal file
@ -0,0 +1,128 @@
|
||||
//
|
||||
// MultilineTextFieldSizer.swift
|
||||
// Evergreen
|
||||
//
|
||||
// Created by Brent Simmons on 2/19/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
|
||||
// Get the height of an NSTextField given an NSAttributedString and a width.
|
||||
// Uses a cache. Avoids actually measuring text as much as possible.
|
||||
// Main thread only.
|
||||
|
||||
typealias WidthHeightCache = [Int: Int] // width: height
|
||||
|
||||
final class MultilineTextFieldSizer {
|
||||
|
||||
private let numberOfLines: Int
|
||||
private let textField:NSTextField
|
||||
private var cache = [NSAttributedString: WidthHeightCache]() // Each string has a cache.
|
||||
private static var sizers = [Int: MultilineTextFieldSizer]()
|
||||
|
||||
private init(numberOfLines: Int) {
|
||||
|
||||
self.numberOfLines = numberOfLines
|
||||
self.textField = MultilineTextFieldSizer.createTextField(numberOfLines)
|
||||
}
|
||||
|
||||
static func size(for attributedString: NSAttributedString, numberOfLines: Int, width: Int) -> Int {
|
||||
|
||||
return sizer(numberOfLines: numberOfLines).height(for: attributedString, width: width)
|
||||
}
|
||||
|
||||
static func emptyCache() {
|
||||
|
||||
sizers = [Int: MultilineTextFieldSizer]()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
|
||||
private extension MultilineTextFieldSizer {
|
||||
|
||||
static func sizer(numberOfLines: Int) -> MultilineTextFieldSizer {
|
||||
|
||||
if let cachedSizer = sizers[numberOfLines] {
|
||||
return cachedSizer
|
||||
}
|
||||
|
||||
let newSizer = MultilineTextFieldSizer(numberOfLines: numberOfLines)
|
||||
sizers[numberOfLines] = newSizer
|
||||
return newSizer
|
||||
}
|
||||
|
||||
func height(for attributedString: NSAttributedString, width: Int) -> Int {
|
||||
|
||||
if cache[attributedString] == nil {
|
||||
cache[attributedString] = WidthHeightCache()
|
||||
}
|
||||
|
||||
if let height = cache[attributedString]![width] {
|
||||
return height
|
||||
}
|
||||
|
||||
if let height = heightConsideringNeighbors(cache[attributedString]!, width) {
|
||||
return height
|
||||
}
|
||||
|
||||
let height = calculateHeight(attributedString, width)
|
||||
cache[attributedString]![width] = height
|
||||
|
||||
return height
|
||||
}
|
||||
|
||||
static func createTextField(_ numberOfLines: Int) -> NSTextField {
|
||||
|
||||
let textField = NSTextField(wrappingLabelWithString: "")
|
||||
textField.usesSingleLineMode = false
|
||||
textField.maximumNumberOfLines = numberOfLines
|
||||
textField.isEditable = false
|
||||
|
||||
return textField
|
||||
}
|
||||
|
||||
func calculateHeight(_ attributedString: NSAttributedString, _ width: Int) -> Int {
|
||||
|
||||
textField.attributedStringValue = attributedString
|
||||
textField.preferredMaxLayoutWidth = CGFloat(width)
|
||||
let size = textField.fittingSize
|
||||
return Int(ceil(size.height))
|
||||
}
|
||||
|
||||
// func widthHeightCache(for attributedString: NSAttributedString) -> WidthHeightCache {
|
||||
//
|
||||
// if let foundCache = cache[attributedString] {
|
||||
// return foundCache
|
||||
// }
|
||||
// let newCache = WidthHeightCache()
|
||||
// cache[attributedString] = newCache
|
||||
// return newCache
|
||||
// }
|
||||
|
||||
func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? {
|
||||
|
||||
// Given width, if the height at width - something and width + something is equal,
|
||||
// then that height must be correct for the given width.
|
||||
|
||||
var smallNeighbor = (width: 0, height: 0)
|
||||
var largeNeighbor = (width: 0, height: 0)
|
||||
|
||||
for (oneWidth, oneHeight) in heightCache {
|
||||
|
||||
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
|
||||
smallNeighbor = (oneWidth, oneHeight)
|
||||
}
|
||||
else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
|
||||
largeNeighbor = (oneWidth, oneHeight)
|
||||
}
|
||||
|
||||
if smallNeighbor.width != 0 && smallNeighbor.height == largeNeighbor.height {
|
||||
return smallNeighbor.height
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
@ -10,6 +10,7 @@ import AppKit
|
||||
|
||||
// Get the size of an NSTextField configured with a specific font with a specific size.
|
||||
// Uses a cache.
|
||||
// Main thready only.
|
||||
|
||||
final class SingleLineTextFieldSizer {
|
||||
|
||||
@ -53,8 +54,15 @@ final class SingleLineTextFieldSizer {
|
||||
return newSizer
|
||||
}
|
||||
|
||||
// Use this call. It’s easiest.
|
||||
|
||||
static func size(for text: String, font: NSFont) -> NSSize {
|
||||
|
||||
return sizer(for: font).size(for: text)
|
||||
}
|
||||
|
||||
static func emptyCache() {
|
||||
|
||||
sizers = [NSFont: SingleLineTextFieldSizer]()
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@ struct TimelineCellAppearance: Equatable {
|
||||
let titleColor: NSColor
|
||||
let titleFont: NSFont
|
||||
let titleBottomMargin: CGFloat
|
||||
let titleNumberOfLines = 2
|
||||
|
||||
let textColor: NSColor
|
||||
let textFont: NSFont
|
||||
|
@ -40,14 +40,14 @@ struct TimelineCellLayout {
|
||||
|
||||
var textBoxRect = TimelineCellLayout.rectForTextBox(appearance, cellData, width)
|
||||
|
||||
let (titleRect, titleLine1Rect) = TimelineCellLayout.rectsForTitle(textBoxRect, cellData)
|
||||
let titleRect = TimelineCellLayout.rectForTitle(textBoxRect, cellData)
|
||||
let dateRect = TimelineCellLayout.rectForDate(textBoxRect, titleRect, appearance, cellData)
|
||||
let feedNameRect = TimelineCellLayout.rectForFeedName(textBoxRect, dateRect, appearance, cellData)
|
||||
let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, titleLine1Rect)
|
||||
let starRect = TimelineCellLayout.rectForStar(appearance, unreadIndicatorRect)
|
||||
|
||||
textBoxRect.size.height = ceil([titleRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
|
||||
let avatarImageRect = TimelineCellLayout.rectForAvatar(cellData, appearance, textBoxRect, width)
|
||||
let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, titleRect)
|
||||
let starRect = TimelineCellLayout.rectForStar(appearance, unreadIndicatorRect)
|
||||
|
||||
let paddingBottom = appearance.cellPadding.bottom
|
||||
|
||||
@ -77,18 +77,20 @@ private extension TimelineCellLayout {
|
||||
return textBoxRect
|
||||
}
|
||||
|
||||
static func rectsForTitle(_ textBoxRect: NSRect, _ cellData: TimelineCellData) -> (NSRect, NSRect) {
|
||||
static func rectForTitle(_ textBoxRect: NSRect, _ cellData: TimelineCellData) -> NSRect {
|
||||
|
||||
var r = textBoxRect
|
||||
let renderer = RSMultiLineRenderer(attributedTitle: cellData.attributedTitle)
|
||||
let height = MultilineTextFieldSizer.size(for: cellData.attributedTitle, numberOfLines: 2, width: Int(textBoxRect.width))
|
||||
// let renderer = RSMultiLineRenderer(attributedTitle: cellData.attributedTitle)
|
||||
//
|
||||
// let measurements = renderer.measurements(forWidth: textBoxRect.width)
|
||||
r.size.height = CGFloat(height)
|
||||
|
||||
let measurements = renderer.measurements(forWidth: textBoxRect.width)
|
||||
r.size.height = CGFloat(measurements.height)
|
||||
|
||||
var rline1 = r
|
||||
rline1.size.height = CGFloat(measurements.heightOfFirstLine)
|
||||
|
||||
return (r, rline1)
|
||||
return r
|
||||
// var rline1 = r
|
||||
// rline1.size.height = CGFloat(measurements.heightOfFirstLine)
|
||||
//
|
||||
// return (r, rline1)
|
||||
}
|
||||
|
||||
static func rectForDate(_ textBoxRect: NSRect, _ titleRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
|
||||
@ -122,13 +124,14 @@ private extension TimelineCellLayout {
|
||||
return r
|
||||
}
|
||||
|
||||
static func rectForUnreadIndicator(_ appearance: TimelineCellAppearance, _ titleLine1Rect: NSRect) -> NSRect {
|
||||
static func rectForUnreadIndicator(_ appearance: TimelineCellAppearance, _ titleRect: NSRect) -> NSRect {
|
||||
|
||||
var r = NSZeroRect
|
||||
r.size = NSSize(width: appearance.unreadCircleDimension, height: appearance.unreadCircleDimension)
|
||||
r.origin.x = appearance.cellPadding.left
|
||||
r = RSRectCenteredVerticallyInRect(r, titleLine1Rect)
|
||||
r.origin.y += 1
|
||||
r.origin.y = titleRect.minY + 6
|
||||
// r = RSRectCenteredVerticallyInRect(r, titleRect)
|
||||
// r.origin.y += 1
|
||||
|
||||
return r
|
||||
}
|
||||
@ -139,7 +142,7 @@ private extension TimelineCellLayout {
|
||||
r.size.width = appearance.starDimension
|
||||
r.size.height = appearance.starDimension
|
||||
r.origin.x = floor(unreadIndicatorRect.origin.x - ((appearance.starDimension - appearance.unreadCircleDimension) / 2.0))
|
||||
r.origin.y = unreadIndicatorRect.origin.y - 3.0
|
||||
r.origin.y = unreadIndicatorRect.origin.y - 4.0
|
||||
return r
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user