chromium/chrome/browser/ui/cocoa/dock_icon.mm

// Copyright 2012 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#import "chrome/browser/ui/cocoa/dock_icon.h"

#include <stdint.h>

#include "base/apple/bundle_locations.h"
#include "base/apple/foundation_util.h"
#include "base/check_op.h"
#include "content/public/browser/browser_thread.h"
#include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"

using content::BrowserThread;

namespace {

// The fraction of the size of the dock icon that the badge is.
constexpr CGFloat kBadgeFraction = 0.375f;
constexpr CGFloat kBadgeMargin = 4;
constexpr CGFloat kBadgeStrokeWidth = 6;

constexpr struct {
  CGFloat offset, radius, opacity;
} kBadgeShadows[] = {
    {0, 2, 0.14},
    {2, 2, 0.12},
    {1, 3, 0.2},
};

// The maximum update rate for the dock icon. 200ms = 5fps.
constexpr int64_t kUpdateFrequencyMs = 200;

}  // namespace

// A view that draws our dock tile.
@interface DockTileView : NSView

// Indicates how many downloads are in progress.
@property(nonatomic) int downloads;

// Indicates whether the progress indicator should be in an indeterminate state
// or not.
@property(nonatomic) BOOL indeterminate;

// Indicates the amount of progress made of the download. Ranges from [0..1].
@property(nonatomic) float progress;

@end

@implementation DockTileView

@synthesize downloads = _downloads;
@synthesize indeterminate = _indeterminate;
@synthesize progress = _progress;

- (void)drawRect:(NSRect)dirtyRect {
  // This needs to draw the current app icon, whether it's using the default
  // icon shipped or a custom icon.
  //
  // -[NSWorkspace iconForFile:] works, but it's NSString path-based, and APIs
  // that use those tend to be on the deprecation chopping block.
  //
  // The NSURLEffectiveIconKey resource value works, but it has an error path
  // that needs to be handled.
  //
  // -[NSApplication applicationIconImage] used to fail to return a custom icon
  // if set, which was fixed a while ago, but it returns an NSImage with a
  // single image rep, 32 pixels wide, which isn't good enough for detail work.
  //
  // Therefore, use [NSImage imageNamed:NSImageNameApplicationIcon].

  NSImage* appIcon = [NSImage imageNamed:NSImageNameApplicationIcon];
  [appIcon drawInRect:self.bounds
             fromRect:NSZeroRect
            operation:NSCompositingOperationSourceOver
             fraction:1.0];

  if (_downloads == 0) {
    return;
  }

  const CGFloat badgeSize = NSWidth(self.bounds) * kBadgeFraction;
  const NSRect badgeRect =
      NSMakeRect(NSMaxX(self.bounds) - badgeSize - kBadgeMargin, kBadgeMargin,
                 badgeSize, badgeSize);
  const CGFloat badgeRadius = badgeSize / 2;
  const NSPoint badgeCenter = NSMakePoint(NSMidX(badgeRect), NSMidY(badgeRect));

  NSBezierPath* backgroundPath =
      [NSBezierPath bezierPathWithOvalInRect:badgeRect];
  [NSColor.clearColor setFill];

  NSShadow* shadow = [[NSShadow alloc] init];
  shadow.shadowColor = NSColor.blackColor;
  for (const auto shadowProps : kBadgeShadows) {
    gfx::ScopedNSGraphicsContextSaveGState scopedGState;
    shadow.shadowOffset = NSMakeSize(0, -shadowProps.offset);
    shadow.shadowBlurRadius = shadowProps.radius;
    [[NSColor colorWithCalibratedWhite:0 alpha:shadowProps.opacity] setFill];
    [shadow set];
    [backgroundPath fill];
  }

  [[NSColor colorWithCalibratedRed:0xec / 255.0
                             green:0xf3 / 255.0
                              blue:0xfe / 255.0
                             alpha:1] setFill];
  [backgroundPath fill];

  // Stroke
  if (!_indeterminate) {
    NSBezierPath* strokePath;
    if (_progress >= 1.0) {
      strokePath = [NSBezierPath bezierPathWithOvalInRect:badgeRect];
    } else {
      CGFloat endAngle = 90.0 - 360.0 * _progress;
      if (endAngle < 0.0) {
        endAngle += 360.0;
      }
      strokePath = [NSBezierPath bezierPath];
      [strokePath
          appendBezierPathWithArcWithCenter:badgeCenter
                                     radius:badgeRadius - kBadgeStrokeWidth / 2
                                 startAngle:90.0
                                   endAngle:endAngle
                                  clockwise:YES];
    }
    [strokePath setLineWidth:kBadgeStrokeWidth];
    // This color is GoogleBlue600, which matches the stroke color for the
    // progress ring of the download toolbar icon in a light theme.
    [[NSColor colorWithSRGBRed:0x1a / 255.0
                         green:0x73 / 255.0
                          blue:0xe8 / 255.0
                         alpha:1] setStroke];
    [strokePath stroke];
  }

  // Download count
  NSNumberFormatter* formatter = [[NSNumberFormatter alloc] init];
  NSString* countString = [formatter stringFromNumber:@(_downloads)];

  CGFloat countFontSize = 24;
  NSSize countSize = NSZeroSize;
  NSAttributedString* countAttrString = nil;
  while (true) {
    NSFont* countFont = [NSFont systemFontOfSize:countFontSize
                                          weight:NSFontWeightMedium];

    // This will generally be plain Helvetica.
    if (!countFont) {
      countFont = [NSFont userFontOfSize:countFontSize];
    }

    // Continued failure would generate an NSException.
    if (!countFont) {
      break;
    }

    countAttrString = [[NSAttributedString alloc]
        initWithString:countString
            attributes:@{
              NSForegroundColorAttributeName :
                  [NSColor colorWithCalibratedWhite:0 alpha:0.65],
              NSFontAttributeName : countFont,
            }];
    countSize = [countAttrString size];
    if (countSize.width > (badgeRadius - kBadgeStrokeWidth) * 1.5) {
      countFontSize -= 1.0;
    } else {
      break;
    }
  }

  NSPoint countOrigin = badgeCenter;
  countOrigin.x -= countSize.width / 2;
  countOrigin.y -= countSize.height / 2;

  [countAttrString drawAtPoint:countOrigin];
}

@end

@implementation DockIcon {
  // The time that the icon was last updated.
  base::TimeTicks _lastUpdate;

  // If true, the state has changed in a significant way since the last icon
  // update and throttling should not prevent icon redraw.
  BOOL _forceUpdate;
}

+ (DockIcon*)sharedDockIcon {
  static DockIcon* icon;
  if (!icon) {
    NSDockTile* dockTile = [NSApp dockTile];

    dockTile.contentView = [[DockTileView alloc] init];

    icon = [[DockIcon alloc] init];
  }

  return icon;
}

- (void)updateIcon {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  static base::TimeDelta updateFrequency =
      base::Milliseconds(kUpdateFrequencyMs);

  base::TimeTicks now = base::TimeTicks::Now();
  base::TimeDelta timeSinceLastUpdate = now - _lastUpdate;
  if (!_forceUpdate && timeSinceLastUpdate < updateFrequency) {
    return;
  }

  _lastUpdate = now;
  _forceUpdate = NO;

  NSDockTile* dockTile = NSApp.dockTile;

  [dockTile display];
}

- (void)setDownloads:(int)downloads {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DockTileView* dockTileView =
      base::apple::ObjCCast<DockTileView>(NSApp.dockTile.contentView);

  if (downloads != [dockTileView downloads]) {
    [dockTileView setDownloads:downloads];
    _forceUpdate = YES;
  }
}

- (void)setIndeterminate:(BOOL)indeterminate {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DockTileView* dockTileView =
      base::apple::ObjCCast<DockTileView>(NSApp.dockTile.contentView);

  if (indeterminate != [dockTileView indeterminate]) {
    [dockTileView setIndeterminate:indeterminate];
    _forceUpdate = YES;
  }
}

- (void)setProgress:(float)progress {
  DCHECK_CURRENTLY_ON(BrowserThread::UI);
  DockTileView* dockTileView =
      base::apple::ObjCCast<DockTileView>(NSApp.dockTile.contentView);

  [dockTileView setProgress:progress];
}

@end