1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
5 #import "chrome/browser/ui/cocoa/tabs/tab_view.h"
7 #include "base/i18n/rtl.h"
8 #include "base/logging.h"
9 #include "base/mac/sdk_forward_declarations.h"
10 #include "base/strings/sys_string_conversions.h"
11 #include "chrome/browser/themes/theme_service.h"
12 #import "chrome/browser/ui/cocoa/tabs/media_indicator_button_cocoa.h"
13 #import "chrome/browser/ui/cocoa/tabs/tab_controller.h"
14 #import "chrome/browser/ui/cocoa/tabs/tab_window_controller.h"
15 #import "chrome/browser/ui/cocoa/themed_window.h"
16 #import "chrome/browser/ui/cocoa/view_id_util.h"
17 #include "chrome/grit/generated_resources.h"
18 #include "grit/theme_resources.h"
19 #import "third_party/google_toolbox_for_mac/src/AppKit/GTMFadeTruncatingTextFieldCell.h"
20 #import "ui/base/cocoa/nsgraphics_context_additions.h"
21 #import "ui/base/cocoa/nsview_additions.h"
22 #include "ui/base/cocoa/three_part_image.h"
23 #include "ui/base/l10n/l10n_util.h"
24 #include "ui/base/resource/resource_bundle.h"
25 #include "ui/gfx/scoped_ns_graphics_context_save_gstate_mac.h"
28 const int kFillHeight = 25; // Height of the "mask on" part of the mask bitmap.
30 // The amount of time in seconds during which each type of glow increases, holds
31 // steady, and decreases, respectively.
32 const NSTimeInterval kHoverShowDuration = 0.2;
33 const NSTimeInterval kHoverHoldDuration = 0.02;
34 const NSTimeInterval kHoverHideDuration = 0.4;
35 const NSTimeInterval kAlertShowDuration = 0.4;
36 const NSTimeInterval kAlertHoldDuration = 0.4;
37 const NSTimeInterval kAlertHideDuration = 0.4;
39 // The default time interval in seconds between glow updates (when
40 // increasing/decreasing).
41 const NSTimeInterval kGlowUpdateInterval = 0.025;
43 // This is used to judge whether the mouse has moved during rapid closure; if it
44 // has moved less than the threshold, we want to close the tab.
45 const CGFloat kRapidCloseDist = 2.5;
49 ui::ThreePartImage& GetMaskImage() {
50 CR_DEFINE_STATIC_LOCAL(ui::ThreePartImage, mask,
51 (IDR_TAB_ALPHA_LEFT, 0, IDR_TAB_ALPHA_RIGHT));
55 ui::ThreePartImage& GetStrokeImage(bool active) {
56 CR_DEFINE_STATIC_LOCAL(
57 ui::ThreePartImage, activeStroke,
58 (IDR_TAB_ACTIVE_LEFT, IDR_TAB_ACTIVE_CENTER, IDR_TAB_ACTIVE_RIGHT));
59 CR_DEFINE_STATIC_LOCAL(
60 ui::ThreePartImage, inactiveStroke,
61 (IDR_TAB_INACTIVE_LEFT, IDR_TAB_INACTIVE_CENTER, IDR_TAB_INACTIVE_RIGHT));
63 return active ? activeStroke : inactiveStroke;
68 @interface TabView(Private)
70 - (void)resetLastGlowUpdateTime;
71 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate;
72 - (void)adjustGlowValue;
74 @end // TabView(Private)
76 @implementation TabView
78 @synthesize state = state_;
79 @synthesize hoverAlpha = hoverAlpha_;
80 @synthesize alertAlpha = alertAlpha_;
81 @synthesize closing = closing_;
83 - (id)initWithFrame:(NSRect)frame
84 controller:(TabController*)controller
85 closeButton:(HoverCloseButton*)closeButton {
86 self = [super initWithFrame:frame];
88 controller_ = controller;
89 closeButton_ = closeButton;
91 // Make a text field for the title, but don't add it as a subview.
92 // We will use the cell to draw the text directly into our layer,
93 // so that we can get font smoothing enabled.
94 titleView_.reset([[NSTextField alloc] init]);
95 [titleView_ setAutoresizingMask:NSViewWidthSizable];
96 base::scoped_nsobject<GTMFadeTruncatingTextFieldCell> labelCell(
97 [[GTMFadeTruncatingTextFieldCell alloc] initTextCell:@"Label"]);
98 [labelCell setControlSize:NSSmallControlSize];
99 CGFloat fontSize = [NSFont systemFontSizeForControlSize:NSSmallControlSize];
100 [labelCell setFont:[NSFont systemFontOfSize:fontSize]];
101 [titleView_ setCell:labelCell];
102 titleViewCell_ = labelCell;
104 [self setWantsLayer:YES]; // -drawFill: needs a layer.
110 // Cancel any delayed requests that may still be pending (drags or hover).
111 [NSObject cancelPreviousPerformRequestsWithTarget:self];
115 // Called to obtain the context menu for when the user hits the right mouse
116 // button (or control-clicks). (Note that -rightMouseDown: is *not* called for
119 if ([self isClosing])
122 // Sheets, being window-modal, should block contextual menus. For some reason
123 // they do not. Disallow them ourselves.
124 if ([[self window] attachedSheet])
127 return [controller_ menu];
130 - (void)resizeSubviewsWithOldSize:(NSSize)oldBoundsSize {
131 [super resizeSubviewsWithOldSize:oldBoundsSize];
132 // Called when our view is resized. If it gets too small, start by hiding
133 // the close button and only show it if tab is selected. Eventually, hide the
135 [controller_ updateVisibility];
138 // Overridden so that mouse clicks come to this view (the parent of the
139 // hierarchy) first. We want to handle clicks and drags in this class and
140 // leave the background button for display purposes only.
141 - (BOOL)acceptsFirstMouse:(NSEvent*)theEvent {
145 - (void)mouseEntered:(NSEvent*)theEvent {
146 isMouseInside_ = YES;
147 [self resetLastGlowUpdateTime];
148 [self adjustGlowValue];
151 - (void)mouseMoved:(NSEvent*)theEvent {
152 if (state_ == NSOffState) {
153 hoverPoint_ = [self convertPoint:[theEvent locationInWindow] fromView:nil];
154 [self setNeedsDisplay:YES];
158 - (void)mouseExited:(NSEvent*)theEvent {
161 [NSDate timeIntervalSinceReferenceDate] + kHoverHoldDuration;
162 [self resetLastGlowUpdateTime];
163 [self adjustGlowValue];
166 - (void)setTrackingEnabled:(BOOL)enabled {
167 if (![closeButton_ isHidden]) {
168 [closeButton_ setTrackingEnabled:enabled];
172 // Determines which view a click in our frame actually hit. It's either this
173 // view or one of the child buttons.
174 - (NSView*)hitTest:(NSPoint)aPoint {
175 NSView* const defaultHitTestResult = [super hitTest:aPoint];
176 if ([defaultHitTestResult isKindOfClass:[NSButton class]])
177 return defaultHitTestResult;
179 NSPoint viewPoint = [self convertPoint:aPoint fromView:[self superview]];
180 NSRect maskRect = [self bounds];
181 maskRect.size.height = kFillHeight;
182 return GetMaskImage().HitTest(viewPoint, maskRect) ? self : nil;
185 // Handle clicks and drags in this button. We get here because we have
186 // overridden acceptsFirstMouse: and the click is within our bounds.
187 - (void)mouseDown:(NSEvent*)theEvent {
188 if ([self isClosing])
191 // Record the point at which this event happened. This is used by other mouse
192 // events that are dispatched from |-maybeStartDrag::|.
193 mouseDownPoint_ = [theEvent locationInWindow];
195 // Record the state of the close button here, because selecting the tab will
197 BOOL closeButtonActive = ![closeButton_ isHidden];
199 // During the tab closure animation (in particular, during rapid tab closure),
200 // we may get incorrectly hit with a mouse down. If it should have gone to the
201 // close button, we send it there -- it should then track the mouse, so we
202 // don't have to worry about mouse ups.
203 if (closeButtonActive && [controller_ inRapidClosureMode]) {
204 NSPoint hitLocation = [[self superview] convertPoint:mouseDownPoint_
206 if ([self hitTest:hitLocation] == closeButton_) {
207 [closeButton_ mouseDown:theEvent];
212 // If the tab gets torn off, the tab controller will be removed from the tab
213 // strip and then deallocated. This will also result in *us* being
214 // deallocated. Both these are bad, so we prevent this by retaining the
216 base::scoped_nsobject<TabController> controller([controller_ retain]);
218 // Try to initiate a drag. This will spin a custom event loop and may
219 // dispatch other mouse events.
220 [controller_ maybeStartDrag:theEvent forTab:controller];
222 // The custom loop has ended, so clear the point.
223 mouseDownPoint_ = NSZeroPoint;
226 - (void)mouseUp:(NSEvent*)theEvent {
227 // Check for rapid tab closure.
228 if ([theEvent type] == NSLeftMouseUp) {
229 NSPoint upLocation = [theEvent locationInWindow];
230 CGFloat dx = upLocation.x - mouseDownPoint_.x;
231 CGFloat dy = upLocation.y - mouseDownPoint_.y;
233 // During rapid tab closure (mashing tab close buttons), we may get hit
234 // with a mouse down. As long as the mouse up is over the close button,
235 // and the mouse hasn't moved too much, we close the tab.
236 if (![closeButton_ isHidden] &&
237 (dx*dx + dy*dy) <= kRapidCloseDist*kRapidCloseDist &&
238 [controller_ inRapidClosureMode]) {
239 NSPoint hitLocation =
240 [[self superview] convertPoint:[theEvent locationInWindow]
242 if ([self hitTest:hitLocation] == closeButton_) {
243 [controller_ closeTab:self];
249 // Fire the action to select the tab.
250 [controller_ selectTab:self];
252 // Messaging the drag controller with |-endDrag:| would seem like the right
253 // thing to do here. But, when a tab has been detached, the controller's
254 // target is nil until the drag is finalized. Since |-mouseUp:| gets called
255 // via the manual event loop inside -[TabStripDragController
256 // maybeStartDrag:forTab:], the drag controller can end the dragging session
257 // itself directly after calling this.
260 - (void)otherMouseUp:(NSEvent*)theEvent {
261 if ([self isClosing])
264 // Support middle-click-to-close.
265 if ([theEvent buttonNumber] == 2) {
266 // |-hitTest:| takes a location in the superview's coordinates.
268 [[self superview] convertPoint:[theEvent locationInWindow]
270 // If the mouse up occurred in our view or over the close button, then
272 if ([self hitTest:upLocation])
273 [controller_ closeTab:self];
277 // Returns the color used to draw the background of a tab. |selected| selects
278 // between the foreground and background tabs.
279 - (NSColor*)backgroundColorForSelected:(bool)selected {
280 ThemeService* themeProvider =
281 static_cast<ThemeService*>([[self window] themeProvider]);
283 return [[self window] backgroundColor];
285 int bitmapResources[2][2] = {
286 // Background window.
288 IDR_THEME_TAB_BACKGROUND_INACTIVE, // Background tab.
289 IDR_THEME_TOOLBAR_INACTIVE, // Active tab.
291 // Currently focused window.
293 IDR_THEME_TAB_BACKGROUND, // Background tab.
294 IDR_THEME_TOOLBAR, // Active tab.
298 // Themes don't have an inactive image so only look for one if there's no
301 [[self window] isMainWindow] || !themeProvider->UsingDefaultTheme();
302 return themeProvider->GetNSImageColorNamed(bitmapResources[active][selected]);
305 // Draws the tab background.
306 - (void)drawFill:(NSRect)dirtyRect {
307 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
308 NSRect bounds = [self bounds];
310 NSRect clippingRect = bounds;
311 clippingRect.size.height = kFillHeight;
312 if (state_ != NSOnState) {
313 // Background tabs should not paint over the tab strip separator, which is
314 // two pixels high in both lodpi and hidpi.
315 clippingRect.origin.y = 2 * [self cr_lineWidth];
316 clippingRect.size.height -= clippingRect.origin.y;
318 NSRectClip(clippingRect);
320 NSPoint position = [[self window]
321 themeImagePositionForAlignment:THEME_IMAGE_ALIGN_WITH_TAB_STRIP];
322 [[NSGraphicsContext currentContext] cr_setPatternPhase:position forView:self];
324 [[self backgroundColorForSelected:(state_ != NSOffState)] set];
325 NSRectFill(dirtyRect);
327 if (state_ == NSOffState)
328 [self drawGlow:dirtyRect];
330 // If we filled outside the middle rect, we need to erase what we filled
331 // outside the tab's shape.
332 // This only works if we are drawing to our own backing layer.
333 if (!NSContainsRect(GetMaskImage().GetMiddleRect(bounds), dirtyRect)) {
334 DCHECK([self layer]);
335 GetMaskImage().DrawInRect(bounds, NSCompositeDestinationIn, 1.0);
339 // Draw the glow for hover and the overlay for alerts.
340 - (void)drawGlow:(NSRect)dirtyRect {
341 NSGraphicsContext* context = [NSGraphicsContext currentContext];
342 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
344 CGFloat hoverAlpha = [self hoverAlpha];
345 CGFloat alertAlpha = [self alertAlpha];
346 if (hoverAlpha > 0 || alertAlpha > 0) {
347 CGContextBeginTransparencyLayer(cgContext, 0);
349 // The alert glow overlay is like the selected state but at most at most 80%
350 // opaque. The hover glow brings up the overlay's opacity at most 50%.
351 CGFloat backgroundAlpha = 0.8 * alertAlpha;
352 backgroundAlpha += (1 - backgroundAlpha) * 0.5 * hoverAlpha;
353 CGContextSetAlpha(cgContext, backgroundAlpha);
355 [[self backgroundColorForSelected:YES] set];
356 NSRectFill(dirtyRect);
358 // ui::ThemeProvider::HasCustomImage is true only if the theme provides the
359 // image. However, even if the theme doesn't provide a tab background, the
360 // theme machinery will make one if given a frame image. See
361 // BrowserThemePack::GenerateTabBackgroundImages for details.
362 ui::ThemeProvider* themeProvider = [[self window] themeProvider];
363 BOOL hasCustomTheme = themeProvider &&
364 (themeProvider->HasCustomImage(IDR_THEME_TAB_BACKGROUND) ||
365 themeProvider->HasCustomImage(IDR_THEME_FRAME));
366 // Draw a mouse hover gradient for the default themes.
367 if (hoverAlpha > 0) {
368 if (themeProvider && !hasCustomTheme) {
369 base::scoped_nsobject<NSGradient> glow([NSGradient alloc]);
370 [glow initWithStartingColor:[NSColor colorWithCalibratedWhite:1.0
371 alpha:1.0 * hoverAlpha]
372 endingColor:[NSColor colorWithCalibratedWhite:1.0
374 NSRect rect = [self bounds];
375 NSPoint point = hoverPoint_;
376 point.y = NSHeight(rect);
377 [glow drawFromCenter:point
380 radius:NSWidth(rect) / 3.0
381 options:NSGradientDrawsBeforeStartingLocation];
385 CGContextEndTransparencyLayer(cgContext);
389 // Draws the tab outline.
390 - (void)drawStroke:(NSRect)dirtyRect {
391 CGFloat alpha = [[self window] isMainWindow] ? 1.0 : tabs::kImageNoFocusAlpha;
392 GetStrokeImage(state_ == NSOnState)
393 .DrawInRect([self bounds], NSCompositeSourceOver, alpha);
396 - (void)drawRect:(NSRect)dirtyRect {
397 [self drawFill:dirtyRect];
398 [self drawStroke:dirtyRect];
400 // We draw the title string directly instead of using a NSTextField subview.
401 // This is so that we can get font smoothing to work on earlier OS, and even
402 // when the tab background is a pattern image (when using themes).
403 if (![titleView_ isHidden]) {
404 gfx::ScopedNSGraphicsContextSaveGState scopedGState;
405 NSGraphicsContext* context = [NSGraphicsContext currentContext];
406 CGContextRef cgContext = static_cast<CGContextRef>([context graphicsPort]);
407 CGContextSetShouldSmoothFonts(cgContext, true);
408 [[titleView_ cell] drawWithFrame:[titleView_ frame] inView:self];
412 - (void)setFrameOrigin:(NSPoint)origin {
413 // The background color depends on the view's vertical position.
414 if (NSMinY([self frame]) != origin.y)
415 [self setNeedsDisplay:YES];
416 [super setFrameOrigin:origin];
419 - (void)setToolTipText:(NSString*)string {
420 toolTipText_.reset([string copy]);
423 - (NSString*)toolTipText {
427 - (void)viewDidMoveToWindow {
428 [super viewDidMoveToWindow];
430 [controller_ updateTitleColor];
432 // The new window may have different main window status.
433 // This happens when the view is moved into a TabWindowOverlayWindow for
435 [self windowDidChangeActive];
440 return [titleView_ stringValue];
443 - (void)setTitle:(NSString*)title {
444 if ([title isEqualToString:[titleView_ stringValue]])
447 [titleView_ setStringValue:title];
448 [closeButton_ setAccessibilityTitle:title];
450 base::string16 title16 = base::SysNSStringToUTF16(title);
451 bool isRTL = base::i18n::GetFirstStrongCharacterDirection(title16) ==
452 base::i18n::RIGHT_TO_LEFT;
453 titleViewCell_.truncateMode = isRTL ? GTMFadeTruncatingHead
454 : GTMFadeTruncatingTail;
456 [self setNeedsDisplayInRect:[titleView_ frame]];
459 - (NSRect)titleFrame {
460 return [titleView_ frame];
463 - (void)setTitleFrame:(NSRect)titleFrame {
464 NSRect oldTitleFrame = [titleView_ frame];
465 if (NSEqualRects(titleFrame, oldTitleFrame))
467 [titleView_ setFrame:titleFrame];
468 [self setNeedsDisplayInRect:NSUnionRect(titleFrame, oldTitleFrame)];
471 - (NSColor*)titleColor {
472 return [titleView_ textColor];
475 - (void)setTitleColor:(NSColor*)titleColor {
476 if ([titleColor isEqual:[titleView_ textColor]])
478 [titleView_ setTextColor:titleColor];
479 [self setNeedsDisplayInRect:[titleView_ frame]];
482 - (BOOL)titleHidden {
483 return [titleView_ isHidden];
486 - (void)setTitleHidden:(BOOL)titleHidden {
487 if (titleHidden == [titleView_ isHidden])
489 [titleView_ setHidden:titleHidden];
490 [self setNeedsDisplayInRect:[titleView_ frame]];
493 - (void)setState:(NSCellStateValue)state {
497 [self setNeedsDisplay:YES];
500 - (void)setClosing:(BOOL)closing {
501 closing_ = closing; // Safe because the property is nonatomic.
502 // When closing, ensure clicks to the close button go nowhere.
504 [closeButton_ setTarget:nil];
505 [closeButton_ setAction:nil];
510 // Do not start a new alert while already alerting or while in a decay cycle.
511 if (alertState_ == tabs::kAlertNone) {
512 alertState_ = tabs::kAlertRising;
513 [self resetLastGlowUpdateTime];
514 [self adjustGlowValue];
518 - (void)cancelAlert {
519 if (alertState_ != tabs::kAlertNone) {
520 alertState_ = tabs::kAlertFalling;
522 [NSDate timeIntervalSinceReferenceDate] + kGlowUpdateInterval;
523 [self resetLastGlowUpdateTime];
524 [self adjustGlowValue];
528 - (int)widthOfLargestSelectableRegion {
529 // Assume the entire region to the left of the media indicator and/or close
530 // buttons is available for click-to-select. If neither are visible, the
531 // entire tab region is available.
532 MediaIndicatorButton* const indicator = [controller_ mediaIndicatorButton];
533 const int indicatorLeft = (!indicator || [indicator isHidden]) ?
534 NSWidth([self frame]) : NSMinX([indicator frame]);
535 HoverCloseButton* const closeButton = [controller_ closeButton];
536 const int closeButtonLeft = (!closeButton || [closeButton isHidden]) ?
537 NSWidth([self frame]) : NSMinX([closeButton frame]);
538 return std::min(indicatorLeft, closeButtonLeft);
541 - (BOOL)accessibilityIsIgnored {
545 - (NSArray*)accessibilityActionNames {
546 NSArray* parentActions = [super accessibilityActionNames];
548 return [parentActions arrayByAddingObject:NSAccessibilityPressAction];
551 - (NSArray*)accessibilityAttributeNames {
552 NSMutableArray* attributes =
553 [[super accessibilityAttributeNames] mutableCopy];
554 [attributes addObject:NSAccessibilityTitleAttribute];
555 [attributes addObject:NSAccessibilityEnabledAttribute];
556 [attributes addObject:NSAccessibilityValueAttribute];
558 return [attributes autorelease];
561 - (BOOL)accessibilityIsAttributeSettable:(NSString*)attribute {
562 if ([attribute isEqual:NSAccessibilityTitleAttribute])
565 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
568 if ([attribute isEqual:NSAccessibilityValueAttribute])
571 return [super accessibilityIsAttributeSettable:attribute];
574 - (void)accessibilityPerformAction:(NSString*)action {
575 if ([action isEqual:NSAccessibilityPressAction] &&
576 [[controller_ target] respondsToSelector:[controller_ action]]) {
577 [[controller_ target] performSelector:[controller_ action]
579 NSAccessibilityPostNotification(self,
580 NSAccessibilityValueChangedNotification);
582 [super accessibilityPerformAction:action];
586 - (id)accessibilityAttributeValue:(NSString*)attribute {
587 if ([attribute isEqual:NSAccessibilityRoleAttribute])
588 return NSAccessibilityRadioButtonRole;
589 if ([attribute isEqual:NSAccessibilityRoleDescriptionAttribute])
590 return l10n_util::GetNSStringWithFixup(IDS_ACCNAME_TAB);
591 if ([attribute isEqual:NSAccessibilityTitleAttribute])
592 return [controller_ title];
593 if ([attribute isEqual:NSAccessibilityValueAttribute])
594 return [NSNumber numberWithInt:[controller_ selected]];
595 if ([attribute isEqual:NSAccessibilityEnabledAttribute])
596 return [NSNumber numberWithBool:YES];
598 return [super accessibilityAttributeValue:attribute];
605 // ThemedWindowDrawing implementation.
607 - (void)windowDidChangeTheme {
608 [self setNeedsDisplay:YES];
611 - (void)windowDidChangeActive {
612 [self setNeedsDisplay:YES];
615 @end // @implementation TabView
617 @implementation TabView (TabControllerInterface)
619 - (void)setController:(TabController*)controller {
620 controller_ = controller;
623 @end // @implementation TabView (TabControllerInterface)
625 @implementation TabView(Private)
627 - (void)resetLastGlowUpdateTime {
628 lastGlowUpdate_ = [NSDate timeIntervalSinceReferenceDate];
631 - (NSTimeInterval)timeElapsedSinceLastGlowUpdate {
632 return [NSDate timeIntervalSinceReferenceDate] - lastGlowUpdate_;
635 - (void)adjustGlowValue {
636 // A time interval long enough to represent no update.
637 const NSTimeInterval kNoUpdate = 1000000;
639 // Time until next update for either glow.
640 NSTimeInterval nextUpdate = kNoUpdate;
642 NSTimeInterval elapsed = [self timeElapsedSinceLastGlowUpdate];
643 NSTimeInterval currentTime = [NSDate timeIntervalSinceReferenceDate];
645 // TODO(viettrungluu): <http://crbug.com/30617> -- split off the stuff below
646 // into a pure function and add a unit test.
648 CGFloat hoverAlpha = [self hoverAlpha];
649 if (isMouseInside_) {
650 // Increase hover glow until it's 1.
651 if (hoverAlpha < 1) {
652 hoverAlpha = MIN(hoverAlpha + elapsed / kHoverShowDuration, 1);
653 [self setHoverAlpha:hoverAlpha];
654 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
655 } // Else already 1 (no update needed).
657 if (currentTime >= hoverHoldEndTime_) {
658 // No longer holding, so decrease hover glow until it's 0.
659 if (hoverAlpha > 0) {
660 hoverAlpha = MAX(hoverAlpha - elapsed / kHoverHideDuration, 0);
661 [self setHoverAlpha:hoverAlpha];
662 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
663 } // Else already 0 (no update needed).
665 // Schedule update for end of hold time.
666 nextUpdate = MIN(hoverHoldEndTime_ - currentTime, nextUpdate);
670 CGFloat alertAlpha = [self alertAlpha];
671 if (alertState_ == tabs::kAlertRising) {
672 // Increase alert glow until it's 1 ...
673 alertAlpha = MIN(alertAlpha + elapsed / kAlertShowDuration, 1);
674 [self setAlertAlpha:alertAlpha];
676 // ... and having reached 1, switch to holding.
677 if (alertAlpha >= 1) {
678 alertState_ = tabs::kAlertHolding;
679 alertHoldEndTime_ = currentTime + kAlertHoldDuration;
680 nextUpdate = MIN(kAlertHoldDuration, nextUpdate);
682 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
684 } else if (alertState_ != tabs::kAlertNone) {
685 if (alertAlpha > 0) {
686 if (currentTime >= alertHoldEndTime_) {
687 // Stop holding, then decrease alert glow (until it's 0).
688 if (alertState_ == tabs::kAlertHolding) {
689 alertState_ = tabs::kAlertFalling;
690 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
692 DCHECK_EQ(tabs::kAlertFalling, alertState_);
693 alertAlpha = MAX(alertAlpha - elapsed / kAlertHideDuration, 0);
694 [self setAlertAlpha:alertAlpha];
695 nextUpdate = MIN(kGlowUpdateInterval, nextUpdate);
698 // Schedule update for end of hold time.
699 nextUpdate = MIN(alertHoldEndTime_ - currentTime, nextUpdate);
702 // Done the alert decay cycle.
703 alertState_ = tabs::kAlertNone;
707 if (nextUpdate < kNoUpdate)
708 [self performSelector:_cmd withObject:nil afterDelay:nextUpdate];
710 [self resetLastGlowUpdateTime];
711 [self setNeedsDisplay:YES];
714 @end // @implementation TabView(Private)