Continue progress toward variable row heights.

This commit is contained in:
Brent Simmons 2018-02-21 22:48:34 -08:00
parent ab6d232377
commit d3846a6a37
9 changed files with 86 additions and 76 deletions

View File

@ -164,7 +164,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
func applicationDidResignActive(_ notification: Notification) {
TimelineCellData.emptyCache()
timelineEmptyCaches()
saveState()

View File

@ -90,7 +90,17 @@ private extension MultilineTextFieldSizer {
return newSizer
}
func sizeInfo(for string: String, width: Int) -> Int {
func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo {
let textFieldHeight = height(for: string, width: width)
let numberOfLinesUsed = numberOfLines(for: textFieldHeight)
let size = NSSize(width: width, height: textFieldHeight)
let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed)
return sizeInfo
}
func height(for string: String, width: Int) -> Int {
if cache[string] == nil {
cache[string] = WidthHeightCache()
@ -135,6 +145,15 @@ private extension MultilineTextFieldSizer {
return Int(ceil(size.height))
}
func numberOfLines(for height: Int) -> Int {
// Well have to see if this really works reliably.
let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0
let lines = Int(round(CGFloat(height) / averageHeight))
return lines
}
func heightIsProbablySingleLineHeight(_ height: Int) -> Bool {
return heightIsProbablyEqualToEstimate(height, singleLineHeightEstimate)

View File

@ -23,7 +23,7 @@ struct TimelineCellAppearance: Equatable {
let titleColor: NSColor
let titleFont: NSFont
let titleBottomMargin: CGFloat
let titleNumberOfLines = 2
let titleNumberOfLines: Int
let textColor: NSColor
let textFont: NSFont
@ -67,7 +67,8 @@ struct TimelineCellAppearance: Equatable {
self.titleColor = theme.color(forKey: "MainWindow.Timeline.cell.titleColor")
self.titleFont = NSFont.systemFont(ofSize: largeItemFontSize, weight: NSFont.Weight.semibold)
self.titleBottomMargin = theme.float(forKey: "MainWindow.Timeline.cell.titleMarginBottom")
self.titleNumberOfLines = theme.integer(forKey: "MainWindow.Timeline.cell.titleMaximumLines")
self.textColor = theme.color(forKey: "MainWindow.Timeline.cell.textColor")
self.textFont = NSFont.systemFont(ofSize: largeItemFontSize)
@ -91,12 +92,10 @@ struct TimelineCellAppearance: Equatable {
self.showAvatar = showAvatar
let margin = self.cellPadding.left + self.unreadCircleDimension + self.unreadCircleMarginRight
// if showAvatar {
// margin += (self.avatarSize.width + self.avatarMarginRight)
// }
self.boxLeftMargin = margin
}
// TODO: update the below
static func ==(lhs: TimelineCellAppearance, rhs: TimelineCellAppearance) -> Bool {
return lhs.boxLeftMargin == rhs.boxLeftMargin && lhs.showAvatar == rhs.showAvatar && lhs.cellPadding == rhs.cellPadding && lhs.feedNameColor == rhs.feedNameColor && lhs.feedNameFont == rhs.feedNameFont && lhs.dateColor == rhs.dateColor && lhs.dateMarginLeft == rhs.dateMarginLeft && lhs.dateFont == rhs.dateFont && lhs.titleColor == rhs.titleColor && lhs.titleFont == rhs.titleFont && lhs.titleBottomMargin == rhs.titleBottomMargin && lhs.textColor == rhs.textColor && lhs.textFont == rhs.textFont && lhs.unreadCircleColor == rhs.unreadCircleColor && lhs.unreadCircleDimension == rhs.unreadCircleDimension && lhs.unreadCircleMarginRight == rhs.unreadCircleMarginRight && lhs.gridColor == rhs.gridColor && lhs.avatarSize == rhs.avatarSize && lhs.avatarMarginRight == rhs.avatarMarginRight && lhs.avatarAdjustmentTop == rhs.avatarAdjustmentTop && lhs.avatarCornerRadius == rhs.avatarCornerRadius

View File

@ -9,15 +9,10 @@
import AppKit
import Data
var attributedTitleCache = [String: NSAttributedString]()
var attributedDateCache = [String: NSAttributedString]()
var attributedFeedNameCache = [String: NSAttributedString]()
struct TimelineCellData {
let title: String
let text: String
let attributedTitle: NSAttributedString //title + text
let dateString: String
let feedName: String
let showFeedName: Bool
@ -32,15 +27,6 @@ struct TimelineCellData {
self.title = timelineTruncatedTitle(article)
self.text = timelineTruncatedSummary(article)
let attributedTitleCacheKey = "_title: " + self.title + "_text: " + self.text
if let s = attributedTitleCache[attributedTitleCacheKey] {
self.attributedTitle = s
}
else {
self.attributedTitle = attributedTitleString(title, text, appearance)
attributedTitleCache[attributedTitleCacheKey] = self.attributedTitle
}
self.dateString = timelineDateString(article.logicalDatePublished)
if let feedName = feedName {
@ -63,7 +49,6 @@ struct TimelineCellData {
init() { //Empty
self.title = ""
self.attributedTitle = NSAttributedString(string: "")
self.text = ""
self.dateString = ""
self.feedName = ""
@ -74,31 +59,4 @@ struct TimelineCellData {
self.read = true
self.starred = false
}
static func emptyCache() {
attributedTitleCache = [String: NSAttributedString]()
attributedDateCache = [String: NSAttributedString]()
attributedFeedNameCache = [String: NSAttributedString]()
}
}
let emptyCellData = TimelineCellData()
private func attributedTitleString(_ title: String, _ text: String, _ appearance: TimelineCellAppearance) -> NSAttributedString {
if !title.isEmpty && !text.isEmpty {
let titleMutable = NSMutableAttributedString(string: title, attributes: [NSAttributedStringKey.foregroundColor: appearance.titleColor, NSAttributedStringKey.font: appearance.titleFont])
// let attributedText = NSAttributedString(string: " " + text, attributes: [NSAttributedStringKey.foregroundColor: appearance.textColor, NSAttributedStringKey.font: appearance.textFont])
// titleMutable.append(attributedText)
return titleMutable
}
if !title.isEmpty && text.isEmpty {
return NSAttributedString(string: title, attributes: [NSAttributedStringKey.foregroundColor: appearance.titleColor, NSAttributedStringKey.font: appearance.titleFont])
}
return NSAttributedString(string: text, attributes: [NSAttributedStringKey.foregroundColor: appearance.textOnlyColor, NSAttributedStringKey.font: appearance.textOnlyFont])
}

View File

@ -16,41 +16,49 @@ struct TimelineCellLayout {
let feedNameRect: NSRect
let dateRect: NSRect
let titleRect: NSRect
let numberOfLinesForTitle: Int
let summaryRect: NSRect
let textRect: NSRect
let unreadIndicatorRect: NSRect
let starRect: NSRect
let avatarImageRect: NSRect
let paddingBottom: CGFloat
init(width: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, avatarImageRect: NSRect, paddingBottom: CGFloat) {
init(width: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, numberOfLinesForTitle: Int, summaryRect: NSRect, textRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, avatarImageRect: NSRect, paddingBottom: CGFloat) {
self.width = width
self.feedNameRect = feedNameRect
self.dateRect = dateRect
self.titleRect = titleRect
self.numberOfLinesForTitle = numberOfLinesForTitle
self.summaryRect = summaryRect
self.textRect = textRect
self.unreadIndicatorRect = unreadIndicatorRect
self.starRect = starRect
self.avatarImageRect = avatarImageRect
self.paddingBottom = paddingBottom
self.height = [feedNameRect, dateRect, titleRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, avatarImageRect].maxY() + paddingBottom
}
init(width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) {
var textBoxRect = TimelineCellLayout.rectForTextBox(appearance, cellData, width)
let titleRect = TimelineCellLayout.rectForTitle(textBoxRect, cellData)
let (titleRect, numberOfLinesForTitle) = TimelineCellLayout.rectForTitle(textBoxRect, appearance, cellData)
let summaryRect = numberOfLinesForTitle > 0 ? TimelineCellLayout.rectForSummary(textBoxRect, titleRect, numberOfLinesForTitle, appearance, cellData) : NSRect.zero
let textRect = numberOfLinesForTitle > 0 ? NSRect.zero : TimelineCellLayout.rectForText(textBoxRect, appearance, cellData)
let dateRect = TimelineCellLayout.rectForDate(textBoxRect, titleRect, appearance, cellData)
let feedNameRect = TimelineCellLayout.rectForFeedName(textBoxRect, dateRect, appearance, cellData)
textBoxRect.size.height = ceil([titleRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
textBoxRect.size.height = ceil([titleRect, summaryRect, textRect, dateRect, feedNameRect].maxY() - textBoxRect.origin.y)
let avatarImageRect = TimelineCellLayout.rectForAvatar(cellData, appearance, textBoxRect, width)
let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, titleRect)
let unreadIndicatorRect = TimelineCellLayout.rectForUnreadIndicator(appearance, textBoxRect)
let starRect = TimelineCellLayout.rectForStar(appearance, unreadIndicatorRect)
let paddingBottom = appearance.cellPadding.bottom
self.init(width: width, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: paddingBottom)
self.init(width: width, feedNameRect: feedNameRect, dateRect: dateRect, titleRect: titleRect, numberOfLinesForTitle: numberOfLinesForTitle, summaryRect: summaryRect, textRect: textRect, unreadIndicatorRect: unreadIndicatorRect, starRect: starRect, avatarImageRect: avatarImageRect, paddingBottom: paddingBottom)
}
static func height(for width: CGFloat, cellData: TimelineCellData, appearance: TimelineCellAppearance) -> CGFloat {
@ -76,18 +84,52 @@ private extension TimelineCellLayout {
return textBoxRect
}
static func rectForTitle(_ textBoxRect: NSRect, _ cellData: TimelineCellData) -> NSRect {
static func rectForTitle(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> (NSRect, Int) {
var r = textBoxRect
let height = MultilineTextFieldSizer.size(for: cellData.title, font: appearance.titleFont, numberOfLines: 2, width: Int(textBoxRect.width))
r.size.height = CGFloat(height)
if cellData.title.isEmpty {
r.size.height = 0
return (r, 0)
}
let sizeInfo = MultilineTextFieldSizer.size(for: cellData.title, font: appearance.titleFont, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width))
r.size.height = sizeInfo.size.height
if sizeInfo.numberOfLinesUsed < 1 {
r.size.height = 0
}
return (r, sizeInfo.numberOfLinesUsed)
}
static func rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
if titleNumberOfLines >= appearance.titleNumberOfLines || cellData.text.isEmpty {
return NSRect.zero
}
return rectOfLineBelow(titleRect, titleRect, 0, cellData.text, appearance.textFont)
}
static func rectForText(_ textBoxRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
var r = textBoxRect
if cellData.text.isEmpty {
r.size.height = 0
return r
}
let sizeInfo = MultilineTextFieldSizer.size(for: cellData.text, font: appearance.textOnlyFont, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width))
r.size.height = sizeInfo.size.height
if sizeInfo.numberOfLinesUsed < 1 {
r.size.height = 0
}
return r
}
static func rectForDate(_ textBoxRect: NSRect, _ titleRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
static func rectForDate(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
return rectOfLineBelow(textBoxRect, titleRect, appearance.titleBottomMargin, cellData.dateString, appearance.dateFont)
return rectOfLineBelow(textBoxRect, rectAbove, appearance.titleBottomMargin, cellData.dateString, appearance.dateFont)
}
static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {

View File

@ -10,12 +10,13 @@ import Foundation
import Data
import RSParser
// TODO: Dont make all this at top level.
private var truncatedFeedNameCache = [String: String]()
private let truncatedTitleCache = NSMutableDictionary()
private let normalizedTextCache = NSMutableDictionary()
private let textCache = NSMutableDictionary()
private let summaryCache = NSMutableDictionary()
//private var summaryCache = [String: String]()
func timelineEmptyCaches() {

View File

@ -146,7 +146,7 @@ private extension TimelineTableCellView {
textField.usesSingleLineMode = false
textField.maximumNumberOfLines = 2
textField.isEditable = false
textField.lineBreakMode = .byTruncatingTail
// textField.lineBreakMode = .byTruncatingTail
textField.cell?.truncatesLastVisibleLine = true
textField.allowsDefaultTighteningForTruncation = false
return textField
@ -171,6 +171,7 @@ private extension TimelineTableCellView {
else {
feedNameView.textColor = cellAppearance.feedNameColor
dateView.textColor = cellAppearance.dateColor
titleView.textColor = cellAppearance.titleColor
}
}
@ -178,6 +179,7 @@ private extension TimelineTableCellView {
feedNameView.font = cellAppearance.feedNameFont
dateView.font = cellAppearance.dateFont
titleView.font = cellAppearance.titleFont
}
func updateTextFields() {
@ -222,17 +224,7 @@ private extension TimelineTableCellView {
func updateTitleView() {
if isEmphasized && isSelected {
if let attributedTitle = cellData?.attributedTitle {
titleView.attributedStringValue = attributedTitle.rs_attributedStringByMakingTextWhite()
}
}
else {
if let attributedTitle = cellData?.attributedTitle {
titleView.attributedStringValue = attributedTitle
}
}
titleView.stringValue = cellData?.title ?? ""
needsLayout = true
}

View File

@ -133,8 +133,6 @@ class TimelineViewController: NSViewController, UndoableCommandRunner {
private func fontSizeDidChange() {
TimelineCellData.emptyCache()
cellAppearance = TimelineCellAppearance(theme: appDelegate.currentTheme, showAvatar: false, fontSize: fontSize)
cellAppearanceWithAvatar = TimelineCellAppearance(theme: appDelegate.currentTheme, showAvatar: true, fontSize: fontSize)
updateRowHeights()
@ -591,7 +589,7 @@ extension TimelineViewController: NSTableViewDelegate {
private func makeTimelineCellEmpty(_ cell: TimelineTableCellView) {
cell.objectValue = nil
cell.cellData = emptyCellData
cell.cellData = TimelineCellData()
}
}

View File

@ -121,6 +121,8 @@
<integer>7</integer>
<key>starDimension</key>
<integer>13</integer>
<key>titleMaximumLines</key>
<integer>2</integer>
</dict>
</dict>
<key>Detail</key>