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.
4 
5 #include "ui/views/controls/progress_bar.h"
6 
7 #include <algorithm>
8 #include <cmath>
9 #include <memory>
10 #include <string>
11 
12 #include "base/check_op.h"
13 #include "base/i18n/number_formatting.h"
14 #include "base/macros.h"
15 #include "cc/paint/paint_flags.h"
16 #include "third_party/skia/include/core/SkPath.h"
17 #include "third_party/skia/include/effects/SkGradientShader.h"
18 #include "ui/accessibility/ax_enums.mojom.h"
19 #include "ui/accessibility/ax_node_data.h"
20 #include "ui/gfx/animation/linear_animation.h"
21 #include "ui/gfx/canvas.h"
22 #include "ui/gfx/color_utils.h"
23 #include "ui/native_theme/native_theme.h"
24 #include "ui/views/metadata/metadata_impl_macros.h"
25 #include "ui/views/widget/widget.h"
26 
27 namespace views {
28 
29 namespace {
30 
31 // In DP, the amount to round the corners of the progress bar (both bg and
32 // fg, aka slice).
33 constexpr int kCornerRadius = 3;
34 
35 // Adds a rectangle to the path. The corners will be rounded if there is room.
AddPossiblyRoundRectToPath(const gfx::Rect & rectangle,bool allow_round_corner,SkPath * path)36 void AddPossiblyRoundRectToPath(const gfx::Rect& rectangle,
37                                 bool allow_round_corner,
38                                 SkPath* path) {
39   if (!allow_round_corner || rectangle.height() < kCornerRadius) {
40     path->addRect(gfx::RectToSkRect(rectangle));
41   } else {
42     path->addRoundRect(gfx::RectToSkRect(rectangle), kCornerRadius,
43                        kCornerRadius);
44   }
45 }
46 
RoundToPercent(double fractional_value)47 int RoundToPercent(double fractional_value) {
48   return static_cast<int>(fractional_value * 100);
49 }
50 
51 }  // namespace
52 
ProgressBar(int preferred_height,bool allow_round_corner)53 ProgressBar::ProgressBar(int preferred_height, bool allow_round_corner)
54     : preferred_height_(preferred_height),
55       allow_round_corner_(allow_round_corner) {
56   SetFlipCanvasOnPaintForRTLUI(true);
57 }
58 
59 ProgressBar::~ProgressBar() = default;
60 
GetAccessibleNodeData(ui::AXNodeData * node_data)61 void ProgressBar::GetAccessibleNodeData(ui::AXNodeData* node_data) {
62   node_data->role = ax::mojom::Role::kProgressIndicator;
63   if (IsIndeterminate())
64     node_data->RemoveStringAttribute(ax::mojom::StringAttribute::kValue);
65   else
66     node_data->SetValue(base::FormatPercent(RoundToPercent(current_value_)));
67 }
68 
CalculatePreferredSize() const69 gfx::Size ProgressBar::CalculatePreferredSize() const {
70   // The width will typically be ignored.
71   gfx::Size pref_size(1, preferred_height_);
72   gfx::Insets insets = GetInsets();
73   pref_size.Enlarge(insets.width(), insets.height());
74   return pref_size;
75 }
76 
VisibilityChanged(View * starting_from,bool is_visible)77 void ProgressBar::VisibilityChanged(View* starting_from, bool is_visible) {
78   MaybeNotifyAccessibilityValueChanged();
79 }
80 
AddedToWidget()81 void ProgressBar::AddedToWidget() {
82   MaybeNotifyAccessibilityValueChanged();
83 }
84 
OnPaint(gfx::Canvas * canvas)85 void ProgressBar::OnPaint(gfx::Canvas* canvas) {
86   if (IsIndeterminate())
87     return OnPaintIndeterminate(canvas);
88 
89   gfx::Rect content_bounds = GetContentsBounds();
90 
91   // Draw background.
92   SkPath background_path;
93   AddPossiblyRoundRectToPath(content_bounds, allow_round_corner_,
94                              &background_path);
95   cc::PaintFlags background_flags;
96   background_flags.setStyle(cc::PaintFlags::kFill_Style);
97   background_flags.setAntiAlias(true);
98   background_flags.setColor(GetBackgroundColor());
99   canvas->DrawPath(background_path, background_flags);
100 
101   // Draw slice.
102   SkPath slice_path;
103   const int slice_width = static_cast<int>(
104       content_bounds.width() * std::min(current_value_, 1.0) + 0.5);
105   if (slice_width < 1)
106     return;
107 
108   gfx::Rect slice_bounds = content_bounds;
109   slice_bounds.set_width(slice_width);
110   AddPossiblyRoundRectToPath(slice_bounds, allow_round_corner_, &slice_path);
111 
112   cc::PaintFlags slice_flags;
113   slice_flags.setStyle(cc::PaintFlags::kFill_Style);
114   slice_flags.setAntiAlias(true);
115   slice_flags.setColor(GetForegroundColor());
116   canvas->DrawPath(slice_path, slice_flags);
117 }
118 
GetValue() const119 double ProgressBar::GetValue() const {
120   return current_value_;
121 }
122 
SetValue(double value)123 void ProgressBar::SetValue(double value) {
124   double adjusted_value = (value < 0.0 || value > 1.0) ? -1.0 : value;
125 
126   if (adjusted_value == current_value_)
127     return;
128 
129   current_value_ = adjusted_value;
130   if (IsIndeterminate()) {
131     indeterminate_bar_animation_ = std::make_unique<gfx::LinearAnimation>(this);
132     indeterminate_bar_animation_->SetDuration(base::TimeDelta::FromSeconds(2));
133     indeterminate_bar_animation_->Start();
134   } else {
135     indeterminate_bar_animation_.reset();
136     OnPropertyChanged(&current_value_, kPropertyEffectsPaint);
137   }
138 
139   MaybeNotifyAccessibilityValueChanged();
140 }
141 
GetForegroundColor() const142 SkColor ProgressBar::GetForegroundColor() const {
143   if (foreground_color_)
144     return foreground_color_.value();
145 
146   return GetNativeTheme()->GetSystemColor(
147       ui::NativeTheme::kColorId_ProminentButtonColor);
148 }
149 
SetForegroundColor(SkColor color)150 void ProgressBar::SetForegroundColor(SkColor color) {
151   if (foreground_color_ == color)
152     return;
153 
154   foreground_color_ = color;
155   OnPropertyChanged(&foreground_color_, kPropertyEffectsPaint);
156 }
157 
GetBackgroundColor() const158 SkColor ProgressBar::GetBackgroundColor() const {
159   return background_color_.value_or(
160       color_utils::BlendTowardMaxContrast(GetForegroundColor(), 0xCC));
161 }
162 
SetBackgroundColor(SkColor color)163 void ProgressBar::SetBackgroundColor(SkColor color) {
164   if (background_color_ == color)
165     return;
166 
167   background_color_ = color;
168   OnPropertyChanged(&background_color_, kPropertyEffectsPaint);
169 }
170 
AnimationProgressed(const gfx::Animation * animation)171 void ProgressBar::AnimationProgressed(const gfx::Animation* animation) {
172   DCHECK_EQ(animation, indeterminate_bar_animation_.get());
173   DCHECK(IsIndeterminate());
174   SchedulePaint();
175 }
176 
AnimationEnded(const gfx::Animation * animation)177 void ProgressBar::AnimationEnded(const gfx::Animation* animation) {
178   DCHECK_EQ(animation, indeterminate_bar_animation_.get());
179   // Restarts animation.
180   if (IsIndeterminate())
181     indeterminate_bar_animation_->Start();
182 }
183 
IsIndeterminate()184 bool ProgressBar::IsIndeterminate() {
185   return current_value_ < 0.0;
186 }
187 
OnPaintIndeterminate(gfx::Canvas * canvas)188 void ProgressBar::OnPaintIndeterminate(gfx::Canvas* canvas) {
189   gfx::Rect content_bounds = GetContentsBounds();
190 
191   // Draw background.
192   SkPath background_path;
193   AddPossiblyRoundRectToPath(content_bounds, allow_round_corner_,
194                              &background_path);
195   cc::PaintFlags background_flags;
196   background_flags.setStyle(cc::PaintFlags::kFill_Style);
197   background_flags.setAntiAlias(true);
198   background_flags.setColor(GetBackgroundColor());
199   canvas->DrawPath(background_path, background_flags);
200 
201   // Draw slice.
202   SkPath slice_path;
203   double time = indeterminate_bar_animation_->GetCurrentValue();
204 
205   // The animation spec corresponds to the material design lite's parameter.
206   // (cf. https://github.com/google/material-design-lite/)
207   double bar1_left;
208   double bar1_width;
209   double bar2_left;
210   double bar2_width;
211   if (time < 0.50) {
212     bar1_left = time / 2;
213     bar1_width = time * 1.5;
214     bar2_left = 0;
215     bar2_width = 0;
216   } else if (time < 0.75) {
217     bar1_left = time * 3 - 1.25;
218     bar1_width = 0.75 - (time - 0.5) * 3;
219     bar2_left = 0;
220     bar2_width = time - 0.5;
221   } else {
222     bar1_left = 1;
223     bar1_width = 0;
224     bar2_left = (time - 0.75) * 4;
225     bar2_width = 0.25 - (time - 0.75);
226   }
227 
228   int bar1_start_x = std::round(content_bounds.width() * bar1_left);
229   int bar1_end_x = std::round(content_bounds.width() *
230                               std::min(1.0, bar1_left + bar1_width));
231   int bar2_start_x = std::round(content_bounds.width() * bar2_left);
232   int bar2_end_x = std::round(content_bounds.width() *
233                               std::min(1.0, bar2_left + bar2_width));
234 
235   gfx::Rect slice_bounds = content_bounds;
236   slice_bounds.set_x(content_bounds.x() + bar1_start_x);
237   slice_bounds.set_width(bar1_end_x - bar1_start_x);
238   AddPossiblyRoundRectToPath(slice_bounds, allow_round_corner_, &slice_path);
239   slice_bounds.set_x(content_bounds.x() + bar2_start_x);
240   slice_bounds.set_width(bar2_end_x - bar2_start_x);
241   AddPossiblyRoundRectToPath(slice_bounds, allow_round_corner_, &slice_path);
242 
243   cc::PaintFlags slice_flags;
244   slice_flags.setStyle(cc::PaintFlags::kFill_Style);
245   slice_flags.setAntiAlias(true);
246   slice_flags.setColor(GetForegroundColor());
247   canvas->DrawPath(slice_path, slice_flags);
248 }
249 
MaybeNotifyAccessibilityValueChanged()250 void ProgressBar::MaybeNotifyAccessibilityValueChanged() {
251   if (!GetWidget() || !GetWidget()->IsVisible() ||
252       RoundToPercent(current_value_) == last_announced_percentage_) {
253     return;
254   }
255   last_announced_percentage_ = RoundToPercent(current_value_);
256   NotifyAccessibilityEvent(ax::mojom::Event::kValueChanged, true);
257 }
258 
259 BEGIN_METADATA(ProgressBar, View)
260 ADD_PROPERTY_METADATA(SkColor, ForegroundColor)
261 ADD_PROPERTY_METADATA(SkColor, BackgroundColor)
262 END_METADATA
263 
264 }  // namespace views
265