// Copyright 2019 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ash/shelf/hotseat_widget.h" #include #include #include "ash/constants/ash_features.h" #include "ash/focus_cycler.h" #include "ash/keyboard/ui/keyboard_ui_controller.h" #include "ash/public/cpp/shelf_config.h" #include "ash/public/cpp/shelf_model.h" #include "ash/public/cpp/shelf_types.h" #include "ash/public/cpp/wallpaper/wallpaper_controller_observer.h" #include "ash/shelf/hotseat_transition_animator.h" #include "ash/shelf/scrollable_shelf_view.h" #include "ash/shelf/shelf_app_button.h" #include "ash/shelf/shelf_layout_manager.h" #include "ash/shelf/shelf_navigation_widget.h" #include "ash/shelf/shelf_view.h" #include "ash/shell.h" #include "ash/style/system_shadow.h" #include "ash/system/status_area_widget.h" #include "ash/wallpaper/wallpaper_controller_impl.h" #include "ash/wm/overview/overview_controller.h" #include "ash/wm/overview/overview_observer.h" #include "ash/wm/tablet_mode/tablet_mode_controller.h" #include "base/i18n/rtl.h" #include "base/metrics/histogram_macros.h" #include "ui/aura/scoped_window_targeter.h" #include "ui/aura/window_targeter.h" #include "ui/compositor/animation_throughput_reporter.h" #include "ui/compositor/layer_animation_sequence.h" #include "ui/compositor/scoped_layer_animation_settings.h" #include "ui/gfx/color_analysis.h" #include "ui/gfx/color_palette.h" #include "ui/gfx/color_utils.h" #include "ui/gfx/geometry/rounded_corners_f.h" #include "ui/views/background.h" #include "ui/views/highlight_border.h" #include "ui/views/layout/fill_layout.h" #include "ui/views/view_targeter_delegate.h" #include "ui/views/widget/widget_delegate.h" namespace ash { namespace { void DoScopedAnimationSetting( ui::ScopedLayerAnimationSettings* animation_setter) { animation_setter->SetTransitionDuration( ShelfConfig::Get()->shelf_animation_duration()); animation_setter->SetTweenType(gfx::Tween::EASE_OUT); animation_setter->SetPreemptionStrategy( ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); } // Returns whether there is special hotseat animation for |transition|. bool HasSpecialAnimation(HotseatWidget::StateTransition transition) { switch (transition) { case HotseatWidget::StateTransition::kHomeLauncherAndExtended: case HotseatWidget::StateTransition::kHomeLauncherAndHidden: return true; case HotseatWidget::StateTransition::kHiddenAndExtended: case HotseatWidget::StateTransition::kOther: return false; } } // Calculates the state transition type for the given previous state and // the target state. HotseatWidget::StateTransition CalculateHotseatStateTransition( HotseatState previous_state, HotseatState target_state) { if (previous_state == HotseatState::kNone || target_state == HotseatState::kNone) { return HotseatWidget::StateTransition::kOther; } if (previous_state == target_state) return HotseatWidget::StateTransition::kOther; const bool related_to_homelauncher = (previous_state == HotseatState::kShownHomeLauncher || target_state == HotseatState::kShownHomeLauncher); const bool related_to_extended = (previous_state == HotseatState::kExtended || target_state == HotseatState::kExtended); const bool related_to_hidden = (previous_state == HotseatState::kHidden || target_state == HotseatState::kHidden); if (related_to_homelauncher && related_to_extended) return HotseatWidget::StateTransition::kHomeLauncherAndExtended; if (related_to_homelauncher && related_to_hidden) return HotseatWidget::StateTransition::kHomeLauncherAndHidden; if (related_to_extended && related_to_hidden) return HotseatWidget::StateTransition::kHiddenAndExtended; return HotseatWidget::StateTransition::kOther; } // Base class for hotseat animation transition. class HotseatStateTransitionAnimation : public ui::LayerAnimationElement { public: HotseatStateTransitionAnimation(const gfx::Rect& target_bounds_in_screen, double target_opacity, ui::Layer* hotseat_layer, HotseatWidget* hotseat_widget) : ui::LayerAnimationElement( LayerAnimationElement::BOUNDS | LayerAnimationElement::OPACITY, hotseat_layer->GetAnimator()->GetTransitionDuration()), target_widget_bounds_(target_bounds_in_screen), target_opacity_(target_opacity), tween_type_(hotseat_layer->GetAnimator()->tween_type()), hotseat_widget_(hotseat_widget) {} ~HotseatStateTransitionAnimation() override = default; HotseatStateTransitionAnimation(const HotseatStateTransitionAnimation& rhs) = delete; HotseatStateTransitionAnimation& operator=( const HotseatStateTransitionAnimation& rhs) = delete; protected: // ui::LayerAnimationElement: void OnGetTarget(TargetValue* target) const override { target->opacity = target_opacity_; target->bounds = target_widget_bounds_; } ScrollableShelfView* GetScrollableShelfView() { return hotseat_widget_->scrollable_shelf_view(); } // Hotseat widget's target bounds in screen. gfx::Rect target_widget_bounds_; // Hotseat widget's initial opacity. double start_opacity_ = 0.f; // Hotseat widget's target opacity. double target_opacity_ = 0.f; gfx::Tween::Type tween_type_ = gfx::Tween::LINEAR; HotseatWidget* hotseat_widget_ = nullptr; }; // Animation implemented specifically for the transition between the home // launcher state and the extended state. class HomeAndExtendedTransitionAnimation : public HotseatStateTransitionAnimation { public: HomeAndExtendedTransitionAnimation(const gfx::Rect& target_bounds_in_screen, double target_opacity, ui::Layer* hotseat_layer, HotseatWidget* hotseat_widget) : HotseatStateTransitionAnimation(target_bounds_in_screen, target_opacity, hotseat_layer, hotseat_widget) {} ~HomeAndExtendedTransitionAnimation() override = default; HomeAndExtendedTransitionAnimation( const HomeAndExtendedTransitionAnimation& rhs) = delete; HomeAndExtendedTransitionAnimation& operator=( const HomeAndExtendedTransitionAnimation& rhs) = delete; private: // HotseatStateTransitionAnimation: void OnStart(ui::LayerAnimationDelegate* delegate) override { DCHECK(hotseat_widget_->GetShelfView()->shelf()->IsHorizontalAlignment()); ScrollableShelfView* scrollable_shelf_view = GetScrollableShelfView(); scrollable_shelf_view->set_is_padding_configured_externally( /*is_padding_configured_externally=*/true); // Save initial and target padding insets. initial_padding_insets_ = scrollable_shelf_view->edge_padding_insets(); target_padding_insets_ = scrollable_shelf_view->CalculateMirroredEdgePadding( /*use_target_bounds=*/true); // Save initial opacity. start_opacity_ = hotseat_widget_->GetNativeView()->layer()->opacity(); // Save initial hotseat background bounds. initial_hotseat_background_in_screen_ = hotseat_widget_->GetWindowBoundsInScreen(); initial_hotseat_background_in_screen_.Inset(initial_padding_insets_); // Save target hotseat background bounds. target_hotseat_background_in_screen_ = target_widget_bounds_; target_hotseat_background_in_screen_.Inset(target_padding_insets_); } // HotseatStateTransitionAnimation: bool OnProgress(double current, ui::LayerAnimationDelegate* delegate) override { const double tweened = gfx::Tween::CalculateValue(tween_type_, current); // Set scrollable shelf view's padding insets. gfx::Insets insets_in_animation_progress; insets_in_animation_progress.set_left(gfx::Tween::LinearIntValueBetween( tweened, initial_padding_insets_.left(), target_padding_insets_.left())); insets_in_animation_progress.set_right(gfx::Tween::LinearIntValueBetween( tweened, initial_padding_insets_.right(), target_padding_insets_.right())); ScrollableShelfView* scrollable_shelf_view = GetScrollableShelfView(); scrollable_shelf_view->SetEdgePaddingInsets(insets_in_animation_progress); // Update hotseat widget opacity. delegate->SetOpacityFromAnimation( gfx::Tween::DoubleValueBetween(tweened, start_opacity_, target_opacity_), ui::PropertyChangeReason::FROM_ANIMATION); // Calculate the hotseat widget's bounds. const gfx::Rect hotseat_background_in_progress = gfx::Tween::RectValueBetween(tweened, initial_hotseat_background_in_screen_, target_hotseat_background_in_screen_); gfx::Rect widget_bounds_in_progress = hotseat_background_in_progress; widget_bounds_in_progress.Inset( -scrollable_shelf_view->edge_padding_insets()); // Update hotseat widget bounds. delegate->SetBoundsFromAnimation(widget_bounds_in_progress, ui::PropertyChangeReason::FROM_ANIMATION); // Do recovering when the animation ends. if (current == 1.f) { scrollable_shelf_view->set_is_padding_configured_externally( /*is_padding_configured_externally=*/false); } return true; } // HotseatStateTransitionAnimation: void OnAbort(ui::LayerAnimationDelegate* delegate) override { GetScrollableShelfView()->set_is_padding_configured_externally( /*is_padding_configured_externally=*/false); } // Scrollable shelf's initial padding insets. gfx::Insets initial_padding_insets_; // Scrollable shelf's target padding insets. gfx::Insets target_padding_insets_; // Hotseat background's initial bounds in screen. gfx::Rect initial_hotseat_background_in_screen_; // Hotseat background's target bounds in screen. gfx::Rect target_hotseat_background_in_screen_; }; // Animation implemented specifically for the transition between the home // launcher state and the hidden state. class HomeAndHiddenTransitionAnimation : public HotseatStateTransitionAnimation { public: HomeAndHiddenTransitionAnimation(const gfx::Rect& target_bounds_in_screen, double target_opacity, ui::Layer* hotseat_layer, HotseatWidget* hotseat_widget) : HotseatStateTransitionAnimation(target_bounds_in_screen, target_opacity, hotseat_layer, hotseat_widget) {} ~HomeAndHiddenTransitionAnimation() override = default; protected: // HotseatStateTransitionAnimation: void OnStart(ui::LayerAnimationDelegate* delegate) override { DCHECK(hotseat_widget_->GetShelfView()->shelf()->IsHorizontalAlignment()); start_opacity_ = hotseat_widget_->GetNativeView()->layer()->opacity(); if (hotseat_widget_->state() == HotseatState::kHidden) will_be_hidden_ = true; ScrollableShelfView* scrollable_shelf_view = GetScrollableShelfView(); const gfx::Rect current_widget_bounds( hotseat_widget_->GetWindowBoundsInScreen()); // Ensure that hotseat only has vertical movement during animation. if (will_be_hidden_) { animation_initial_bounds_ = current_widget_bounds; animation_target_bounds_ = current_widget_bounds; animation_target_bounds_.set_y(target_widget_bounds_.y()); } else { animation_initial_bounds_ = target_widget_bounds_; animation_initial_bounds_.set_y(current_widget_bounds.y()); // Ensure that hotseat is set with the target bounds at the end of // animation when hotseat is going to show in home launcher. animation_target_bounds_ = target_widget_bounds_; const gfx::Insets target_padding_insets = scrollable_shelf_view->CalculateMirroredEdgePadding( /*use_target_bounds=*/true); scrollable_shelf_view->SetEdgePaddingInsets(target_padding_insets); delegate->SetBoundsFromAnimation( animation_initial_bounds_, ui::PropertyChangeReason::FROM_ANIMATION); } } // HotseatStateTransitionAnimation: bool OnProgress(double current, ui::LayerAnimationDelegate* delegate) override { const double tweened = gfx::Tween::CalculateValue(tween_type_, current); delegate->SetOpacityFromAnimation( gfx::Tween::DoubleValueBetween(tweened, start_opacity_, target_opacity_), ui::PropertyChangeReason::FROM_ANIMATION); const gfx::Rect widget_bounds_in_progress = gfx::Tween::RectValueBetween( tweened, animation_initial_bounds_, animation_target_bounds_); const bool reach_end = current == 1.f; // When hotseat is going to be hidden, |animation_target_bounds_| is not // equal to |target_widget_bounds_|. So hotseat is set with the target // bounds at the end of animation. It does not bring animation regression // since hotseat is invisible to the user when setting bounds. delegate->SetBoundsFromAnimation(will_be_hidden_ && reach_end ? target_widget_bounds_ : widget_bounds_in_progress, ui::PropertyChangeReason::FROM_ANIMATION); return true; } // HotseatStateTransitionAnimation: void OnAbort(ui::LayerAnimationDelegate* delegate) override {} private: // Whether hotseat widget is hidden after state transition animation. bool will_be_hidden_ = false; // Note that |animation_initial_bounds_| and |animation_target_bounds_| may // not be the hotseat's current bounds and |target_widget_bounds_| // respectively. gfx::Rect animation_initial_bounds_; gfx::Rect animation_target_bounds_; }; // Custom window targeter for the hotseat. Used so the hotseat only processes // events that land on the visible portion of the hotseat, and only while the // hotseat is not animating. class HotseatWindowTargeter : public aura::WindowTargeter { public: explicit HotseatWindowTargeter(HotseatWidget* hotseat_widget) : hotseat_widget_(hotseat_widget) {} ~HotseatWindowTargeter() override = default; HotseatWindowTargeter(const HotseatWindowTargeter& other) = delete; HotseatWindowTargeter& operator=(const HotseatWindowTargeter& rhs) = delete; // aura::WindowTargeter: bool SubtreeShouldBeExploredForEvent(aura::Window* window, const ui::LocatedEvent& event) override { // Do not handle events if the hotseat window is animating as it may animate // over other items which want to process events. if (hotseat_widget_->GetLayer()->GetAnimator()->is_animating()) return false; return aura::WindowTargeter::SubtreeShouldBeExploredForEvent(window, event); } bool GetHitTestRects(aura::Window* target, gfx::Rect* hit_test_rect_mouse, gfx::Rect* hit_test_rect_touch) const override { if (target == hotseat_widget_->GetNativeWindow()) { // Shrink the hit bounds from the size of the window to the size of the // hotseat translucent background. gfx::Rect hit_bounds = target->bounds(); hit_bounds.ClampToCenteredSize( hotseat_widget_->GetTranslucentBackgroundSize()); *hit_test_rect_mouse = *hit_test_rect_touch = hit_bounds; return true; } return aura::WindowTargeter::GetHitTestRects(target, hit_test_rect_mouse, hit_test_rect_touch); } private: // Unowned and guaranteed to be not null for the duration of |this|. HotseatWidget* const hotseat_widget_; }; } // namespace class HotseatWidget::DelegateView : public HotseatTransitionAnimator::Observer, public views::WidgetDelegateView, public views::ViewTargeterDelegate, public OverviewObserver, public WallpaperControllerObserver { public: DelegateView() { SetEventTargeter(std::make_unique(this)); SetPaintToLayer(ui::LAYER_NOT_DRAWN); } DelegateView(const DelegateView&) = delete; DelegateView& operator=(const DelegateView&) = delete; ~DelegateView() override; // views::ViewTargetDelegate: View* TargetForRect(View* root, const gfx::Rect& rect) override { // If a context menu for a shelf app button is shown, redirect all events to // the shelf app button. Context menus generally capture all events, but // shelf app buttons' context menu redirect gesture events to the hotseat // widget so shelf app button can continue handling drag events. // See also HotseatWidget::OnGestureEvent(). views::View* item_with_context_menu = scrollable_shelf_view_->shelf_view()->GetShelfItemViewWithContextMenu(); if (item_with_context_menu) return item_with_context_menu; return views::ViewTargeterDelegate::TargetForRect(root, rect); } // Initializes the view. void Init(ScrollableShelfView* scrollable_shelf_view, HotseatWidget* hotseat_widget); // Updates the hotseat background. void UpdateTranslucentBackground(); void SetTranslucentBackground(const gfx::Rect& translucent_background_bounds); // Sets whether the background should be blurred as requested by the argument, // unless the feature flag is disabled or |disable_blur_for_animations_| is // true, in which case this disables background blur. void SetBackgroundBlur(bool enable_blur); // HotseatTransitionAnimator::Observer: void OnHotseatTransitionAnimationWillStart(HotseatState from_state, HotseatState to_state) override; void OnHotseatTransitionAnimationEnded(HotseatState from_state, HotseatState to_state) override; void OnHotseatTransitionAnimationAborted() override; // views::View: void OnThemeChanged() override; // views::WidgetDelegateView: bool CanActivate() const override; // OverviewObserver: void OnOverviewModeWillStart() override; void OnOverviewModeEndingAnimationComplete(bool canceled) override; // WallpaperControllerObserver: void OnWallpaperColorsChanged() override; void set_focus_cycler(FocusCycler* focus_cycler) { focus_cycler_ = focus_cycler; } int background_blur() const { return translucent_background_->layer()->background_blur(); } bool is_translucent_background_visible_for_test() { return translucent_background_->layer()->GetTargetVisibility(); } private: FocusCycler* focus_cycler_ = nullptr; // A background layer that may be visible depending on HotseatState. views::View* translucent_background_ = nullptr; ScrollableShelfView* scrollable_shelf_view_ = nullptr; // unowned. HotseatWidget* hotseat_widget_ = nullptr; // unowned. // Blur is disabled during animations to improve performance. int blur_lock_ = 0; // The most recent color that the |translucent_background_| has been animated // to. SkColor target_color_ = SK_ColorTRANSPARENT; std::unique_ptr shadow_; }; HotseatWidget::DelegateView::~DelegateView() { WallpaperControllerImpl* wallpaper_controller = Shell::Get()->wallpaper_controller(); OverviewController* overview_controller = Shell::Get()->overview_controller(); if (wallpaper_controller) wallpaper_controller->RemoveObserver(this); if (overview_controller) overview_controller->RemoveObserver(this); } void HotseatWidget::DelegateView::Init( ScrollableShelfView* scrollable_shelf_view, HotseatWidget* hotseat_widget) { hotseat_widget_ = hotseat_widget; SetLayoutManager(std::make_unique()); WallpaperControllerImpl* wallpaper_controller = Shell::Get()->wallpaper_controller(); OverviewController* overview_controller = Shell::Get()->overview_controller(); if (wallpaper_controller) wallpaper_controller->AddObserver(this); if (overview_controller) { overview_controller->AddObserver(this); if (overview_controller->InOverviewSession()) ++blur_lock_; } DCHECK(scrollable_shelf_view); scrollable_shelf_view_ = scrollable_shelf_view; // A container view added here is to prevent the `translucent_background_` // being stretched by the fill layout. auto* background_container_view = AddChildViewAt(std::make_unique(), 0); background_container_view->SetEnabled(false); translucent_background_ = background_container_view->AddChildView(std::make_unique()); if (features::IsDarkLightModeEnabled()) { translucent_background_->SetPaintToLayer(); translucent_background_->layer()->SetFillsBoundsOpaquely(false); } else { translucent_background_->SetPaintToLayer(ui::LAYER_SOLID_COLOR); } translucent_background_->layer()->SetName("hotseat/Background"); // Create a shadow and stack at the bottom. shadow_ = SystemShadow::CreateShadowOnTextureLayer( SystemShadow::Type::kElevation12); auto* parent_layer = translucent_background_->layer()->parent(); auto* shadow_layer = shadow_->GetLayer(); parent_layer->Add(shadow_layer); parent_layer->StackAtBottom(shadow_layer); } void HotseatWidget::DelegateView::UpdateTranslucentBackground() { if (!HotseatWidget::ShouldShowHotseatBackground()) { translucent_background_->SetVisible(false); SetBackgroundBlur(false); shadow_->GetLayer()->SetVisible(false); return; } DCHECK(scrollable_shelf_view_); SetTranslucentBackground( scrollable_shelf_view_->GetHotseatBackgroundBounds()); // Hide the shadow when home launcher is showing in tablet mode. if (hotseat_widget_->state() == HotseatState::kShownHomeLauncher) { shadow_->GetLayer()->SetVisible(false); return; } // Update the shadow content bounds and corner radius. shadow_->GetLayer()->SetVisible(true); gfx::Rect background_bounds = translucent_background_->bounds(); shadow_->SetRoundedCornerRadius(background_bounds.height() / 2); shadow_->SetContentBounds(background_bounds); } void HotseatWidget::DelegateView::SetTranslucentBackground( const gfx::Rect& background_bounds) { DCHECK(HotseatWidget::ShouldShowHotseatBackground()); translucent_background_->SetVisible(true); SetBackgroundBlur(/*enable_blur=*/true); auto* animator = translucent_background_->layer()->GetAnimator(); absl::optional reporter; if (hotseat_widget_ && hotseat_widget_->state() != HotseatState::kNone) { reporter.emplace(animator, hotseat_widget_->GetTranslucentBackgroundReportCallback()); } if (ShelfConfig::Get()->GetDefaultShelfColor() != target_color_) { ui::ScopedLayerAnimationSettings color_animation_setter(animator); DoScopedAnimationSetting(&color_animation_setter); target_color_ = ShelfConfig::Get()->GetDefaultShelfColor(); if (features::IsDarkLightModeEnabled()) { translucent_background_->SetBackground( views::CreateSolidBackground(target_color_)); } else { translucent_background_->layer()->SetColor(target_color_); } } // Animate the bounds change if there's a change of width (for instance when // dragging an app into, or out of, the shelf) and meanwhile scrollable // shelf's bounds does not update at the same time. const bool animate_bounds = background_bounds.width() != translucent_background_->bounds().width() && (scrollable_shelf_view_ && !scrollable_shelf_view_->NeedUpdateToTargetBounds()); absl::optional bounds_animation_setter; if (animate_bounds) { bounds_animation_setter.emplace(animator); DoScopedAnimationSetting(&bounds_animation_setter.value()); } const float radius = hotseat_widget_->GetHotseatSize() / 2.0f; gfx::RoundedCornersF rounded_corners = {radius, radius, radius, radius}; if (translucent_background_->layer()->rounded_corner_radii() != rounded_corners) { translucent_background_->layer()->SetRoundedCornerRadius(rounded_corners); if (features::IsDarkLightModeEnabled()) { translucent_background_->SetBorder( std::make_unique( radius, views::HighlightBorder::Type::kHighlightBorder1, /*use_light_colors=*/!features::IsDarkLightModeEnabled())); } } const gfx::Rect mirrored_bounds = GetMirroredRect(background_bounds); if (translucent_background_->layer()->GetTargetBounds() != mirrored_bounds) translucent_background_->SetBoundsRect(mirrored_bounds); } void HotseatWidget::DelegateView::SetBackgroundBlur(bool enable_blur) { if (!features::IsBackgroundBlurEnabled() || blur_lock_ > 0) return; const int blur_radius = enable_blur ? ShelfConfig::Get()->shelf_blur_radius() : 0; if (translucent_background_->layer()->background_blur() != blur_radius) translucent_background_->layer()->SetBackgroundBlur(blur_radius); } void HotseatWidget::DelegateView::OnHotseatTransitionAnimationWillStart( HotseatState from_state, HotseatState to_state) { DCHECK_LE(blur_lock_, 2); SetBackgroundBlur(false); ++blur_lock_; } void HotseatWidget::DelegateView::OnHotseatTransitionAnimationEnded( HotseatState from_state, HotseatState to_state) { DCHECK_GT(blur_lock_, 0); --blur_lock_; SetBackgroundBlur(true); } void HotseatWidget::DelegateView::OnHotseatTransitionAnimationAborted() { DCHECK_GT(blur_lock_, 0); --blur_lock_; } void HotseatWidget::DelegateView::OnThemeChanged() { views::WidgetDelegateView::OnThemeChanged(); // Only update the background when the `scrollable_shelf_view_` is // initialized. if (scrollable_shelf_view_) UpdateTranslucentBackground(); } bool HotseatWidget::DelegateView::CanActivate() const { // We don't want mouse clicks to activate us, but we need to allow // activation when the user is using the keyboard (FocusCycler). return focus_cycler_ && focus_cycler_->widget_activating() == GetWidget(); } void HotseatWidget::DelegateView::OnOverviewModeWillStart() { DCHECK_LE(blur_lock_, 2); SetBackgroundBlur(false); ++blur_lock_; } void HotseatWidget::DelegateView::OnOverviewModeEndingAnimationComplete( bool canceled) { DCHECK_GT(blur_lock_, 0); --blur_lock_; SetBackgroundBlur(true); } void HotseatWidget::DelegateView::OnWallpaperColorsChanged() { UpdateTranslucentBackground(); } //////////////////////////////////////////////////////////////////////////////// // ScopedInStateTransition HotseatWidget::ScopedInStateTransition::ScopedInStateTransition( HotseatWidget* hotseat_widget, HotseatState old_state, HotseatState target_state) : hotseat_widget_(hotseat_widget) { hotseat_widget_->state_transition_in_progress_ = CalculateHotseatStateTransition(old_state, target_state); } HotseatWidget::ScopedInStateTransition::~ScopedInStateTransition() { hotseat_widget_->state_transition_in_progress_.reset(); } //////////////////////////////////////////////////////////////////////////////// // HotseatWidget HotseatWidget::HotseatWidget() : delegate_view_(new DelegateView()) { ShelfConfig::Get()->AddObserver(this); } HotseatWidget::~HotseatWidget() { ui::LayerAnimator* hotseat_layer_animator = GetNativeView()->layer()->GetAnimator(); if (hotseat_layer_animator->is_animating()) hotseat_layer_animator->AbortAllAnimations(); ShelfConfig::Get()->RemoveObserver(this); shelf_->shelf_widget()->hotseat_transition_animator()->RemoveObserver( delegate_view_); // Remove ScrollableShelfView to avoid any children accessing NativeWidget // after its destruction in ~Widget() before RootView clears. // TODO(pbos): This is defensive, consider having children observe // destruction and/or check the result of GetNativeWidget() and others. GetContentsView()->RemoveChildViewT(scrollable_shelf_view_); } bool HotseatWidget::ShouldShowHotseatBackground() { return Shell::Get()->tablet_mode_controller() && Shell::Get()->tablet_mode_controller()->InTabletMode(); } void HotseatWidget::Initialize(aura::Window* container, Shelf* shelf) { DCHECK(container); DCHECK(shelf); shelf_ = shelf; views::Widget::InitParams params( views::Widget::InitParams::TYPE_WINDOW_FRAMELESS); params.name = "HotseatWidget"; params.delegate = delegate_view_; params.opacity = views::Widget::InitParams::WindowOpacity::kTranslucent; params.ownership = views::Widget::InitParams::WIDGET_OWNS_NATIVE_WIDGET; params.parent = container; params.layer_type = ui::LAYER_NOT_DRAWN; Init(std::move(params)); set_focus_on_creation(false); scrollable_shelf_view_ = GetContentsView()->AddChildView( std::make_unique(ShelfModel::Get(), shelf)); delegate_view_->Init(scrollable_shelf_view(), this); delegate_view_->SetEnableArrowKeyTraversal(true); // The initialization of scrollable shelf should update the translucent // background which is stored in |delegate_view_|. So initializes // |scrollable_shelf_view_| after |delegate_view_|. scrollable_shelf_view_->Init(); } void HotseatWidget::OnHotseatTransitionAnimatorCreated( HotseatTransitionAnimator* animator) { shelf_->shelf_widget()->hotseat_transition_animator()->AddObserver( delegate_view_); } void HotseatWidget::OnMouseEvent(ui::MouseEvent* event) { if (event->type() == ui::ET_MOUSE_PRESSED) keyboard::KeyboardUIController::Get()->HideKeyboardImplicitlyByUser(); views::Widget::OnMouseEvent(event); } void HotseatWidget::OnGestureEvent(ui::GestureEvent* event) { if (event->type() == ui::ET_GESTURE_TAP_DOWN) keyboard::KeyboardUIController::Get()->HideKeyboardImplicitlyByUser(); // Context menus for shelf app button forward gesture events to hotseat // widget, so the shelf app button can continue handling drag even after the // context menu starts capturing events. Ignore events not interesting to the // shelf app button in this state. ShelfAppButton* item_with_context_menu = GetShelfView()->GetShelfItemViewWithContextMenu(); if (item_with_context_menu && !ShelfAppButton::ShouldHandleEventFromContextMenu(event)) { event->SetHandled(); return; } if (!event->handled()) views::Widget::OnGestureEvent(event); // Ensure that the app button's drag state gets cleared on gesture end even if // the event doesn't get delivered to the app button. if (item_with_context_menu && event->type() == ui::ET_GESTURE_END) item_with_context_menu->ClearDragStateOnGestureEnd(); } bool HotseatWidget::OnNativeWidgetActivationChanged(bool active) { if (!Widget::OnNativeWidgetActivationChanged(active)) return false; scrollable_shelf_view_->OnFocusRingActivationChanged(active); return true; } void HotseatWidget::OnShelfConfigUpdated() { set_manually_extended(false); } bool HotseatWidget::IsExtended() const { DCHECK(GetShelfView()->shelf()->IsHorizontalAlignment()); const int extended_bottom = display::Screen::GetScreen() ->GetDisplayNearestView(GetShelfView()->GetWidget()->GetNativeView()) .bounds() .bottom() - (ShelfConfig::Get()->shelf_size() + ShelfConfig::Get()->hotseat_bottom_padding()); return GetWindowBoundsInScreen().bottom() == extended_bottom; } void HotseatWidget::FocusFirstOrLastFocusableChild(bool last) { GetShelfView()->FindFirstOrLastFocusableChild(last)->RequestFocus(); } void HotseatWidget::OnTabletModeChanged() { GetShelfView()->OnTabletModeChanged(); } float HotseatWidget::CalculateShelfViewOpacity() const { const float target_opacity = GetShelfView()->shelf()->shelf_layout_manager()->GetOpacity(); // Hotseat's shelf view should not be dimmed if hotseat is kExtended. return (state() == HotseatState::kExtended) ? 1.0f : target_opacity; } void HotseatWidget::UpdateTranslucentBackground() { delegate_view_->UpdateTranslucentBackground(); } int HotseatWidget::CalculateHotseatYInScreen( HotseatState hotseat_target_state) const { DCHECK(shelf_->IsHorizontalAlignment()); int hotseat_distance_from_bottom_of_display = 0; const int hotseat_size = GetHotseatSize(); switch (hotseat_target_state) { case HotseatState::kShownClamshell: hotseat_distance_from_bottom_of_display = hotseat_size; break; case HotseatState::kShownHomeLauncher: // When the hotseat state is HotseatState::kShownHomeLauncher, the home // launcher is showing in tablet mode. Elevate the hotseat a few px to // match the navigation and status area. hotseat_distance_from_bottom_of_display = hotseat_size + ShelfConfig::Get()->hotseat_bottom_padding(); break; case HotseatState::kHidden: // Show the hotseat offscreen. hotseat_distance_from_bottom_of_display = 0; break; case HotseatState::kExtended: // Show the hotseat at its extended position. hotseat_distance_from_bottom_of_display = ShelfConfig::Get()->in_app_shelf_size() + ShelfConfig::Get()->hotseat_bottom_padding() + hotseat_size; break; case HotseatState::kNone: NOTREACHED(); } const int target_shelf_size = shelf_->shelf_widget()->GetTargetBounds().size().height(); const int hotseat_y_in_shelf = -(hotseat_distance_from_bottom_of_display - target_shelf_size); const int shelf_y = shelf_->shelf_widget()->GetTargetBounds().y(); return hotseat_y_in_shelf + shelf_y; } gfx::Size HotseatWidget::CalculateTargetBoundsSize( HotseatState hotseat_target_state) const { const gfx::Rect shelf_bounds = shelf_->shelf_widget()->GetTargetBounds(); // |hotseat_size| is the height in horizontal alignment or the width in // vertical alignment. const int hotseat_size = GetHotseatSize(); if (hotseat_target_state != HotseatState::kShownHomeLauncher && hotseat_target_state != HotseatState::kShownClamshell) { DCHECK(shelf_->IsHorizontalAlignment()); // Give the hotseat more space if it is shown outside of the shelf. return gfx::Size(shelf_bounds.width(), hotseat_size); } const gfx::Size status_size = shelf_->status_area_widget()->GetTargetBounds().size(); const gfx::Rect nav_bounds = shelf_->navigation_widget()->GetVisibleBounds(); // The navigation widget has extra padding on the hotseat side, to center the // buttons inside of it. Make sure to get the extra nav widget padding and // take it into account when calculating the hotseat size. const int nav_widget_padding = nav_bounds.size().IsEmpty() ? 0 : ShelfConfig::Get()->control_button_edge_spacing( true /* is_primary_axis_edge */); // The minimum gap between hotseat widget and other shelf components including // the status area widget and shelf navigation widget (or the edge of display, // if the shelf navigation widget does not show). const int group_margin = ShelfConfig::Get()->GetAppIconGroupMargin(); if (shelf_->IsHorizontalAlignment()) { const int width = shelf_bounds.width() - nav_bounds.size().width() + nav_widget_padding - 2 * group_margin - status_size.width(); return gfx::Size(width, hotseat_size); } const int height = shelf_bounds.height() - nav_bounds.size().height() + nav_widget_padding - 2 * group_margin - status_size.height(); return gfx::Size(hotseat_size, height); } void HotseatWidget::CalculateTargetBounds() { ShelfLayoutManager* layout_manager = shelf_->shelf_layout_manager(); const HotseatState hotseat_target_state = layout_manager->CalculateHotseatState(layout_manager->visibility_state(), layout_manager->auto_hide_state()); const gfx::Size hotseat_target_size = CalculateTargetBoundsSize(hotseat_target_state); if (hotseat_target_state == HotseatState::kShownHomeLauncher) { target_size_for_shown_state_ = hotseat_target_size; } else { target_size_for_shown_state_ = CalculateTargetBoundsSize(HotseatState::kShownHomeLauncher); } const gfx::Rect shelf_bounds = shelf_->shelf_widget()->GetTargetBounds(); const gfx::Rect status_area_bounds = shelf_->status_area_widget()->GetTargetBounds(); // The minimum gap between hotseat widget and other shelf components including // the status area widget and shelf navigation widget (or the edge of display, // if the shelf navigation widget does not show). const int group_margin = ShelfConfig::Get()->GetAppIconGroupMargin(); gfx::Point hotseat_origin; if (shelf_->IsHorizontalAlignment()) { int hotseat_x; if (hotseat_target_state != HotseatState::kShownHomeLauncher && hotseat_target_state != HotseatState::kShownClamshell) { hotseat_x = shelf_bounds.x(); } else { hotseat_x = base::i18n::IsRTL() ? status_area_bounds.right() + group_margin : status_area_bounds.x() - group_margin - hotseat_target_size.width(); } hotseat_origin = gfx::Point(hotseat_x, CalculateHotseatYInScreen(hotseat_target_state)); } else { hotseat_origin = gfx::Point(shelf_bounds.x(), status_area_bounds.y() - group_margin - hotseat_target_size.height()); } target_bounds_ = gfx::Rect(hotseat_origin, hotseat_target_size); // Check whether |target_bounds_| will change the state of app scaling. If // so, update |target_bounds_| here to avoid re-layout later. MaybeAdjustTargetBoundsForAppScaling(hotseat_target_state); } gfx::Rect HotseatWidget::GetTargetBounds() const { return target_bounds_; } void HotseatWidget::UpdateLayout(bool animate) { const LayoutInputs new_layout_inputs = GetLayoutInputs(); if (layout_inputs_ == new_layout_inputs) return; // Never show this widget outside of an active session. if (!new_layout_inputs.is_active_session_state) Hide(); ui::Layer* shelf_view_layer = GetShelfView()->layer(); { ui::ScopedLayerAnimationSettings animation_setter( shelf_view_layer->GetAnimator()); animation_setter.SetTransitionDuration( animate ? ShelfConfig::Get()->shelf_animation_duration() : base::Milliseconds(0)); animation_setter.SetTweenType(gfx::Tween::EASE_OUT); animation_setter.SetPreemptionStrategy( ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); absl::optional reporter; if (animate && state_ != HotseatState::kNone) { reporter.emplace(animation_setter.GetAnimator(), shelf_->GetHotseatTransitionReportCallback(state_)); } shelf_view_layer->SetOpacity(new_layout_inputs.shelf_view_opacity); } // If shelf view is invisible, the hotseat should be as well. Otherwise the // hotseat opacity should be 1.0f to preserve background blur. const double target_opacity = (new_layout_inputs.shelf_view_opacity == 0.f ? 0.f : 1.f); const gfx::Rect& target_bounds = new_layout_inputs.bounds; if (animate) { LayoutHotseatByAnimation(target_opacity, target_bounds); } else { ui::Layer* hotseat_layer = GetNativeView()->layer(); // If the running bounds animation is not aborted, it will be interrupted // and set hotseat widget with the old target bounds which may differ from // |target_bounds| greatly and bring DCHECK errors. For example, // if hotseat animation is interrupted by the bounds setting triggered by // shelf alignment update, hotseat will be caught in an intermediate state // where the shelf alignment is new and the hotseat bounds are old. hotseat_layer->GetAnimator()->AbortAllAnimations(); hotseat_layer->SetOpacity(target_opacity); hotseat_layer->SetTransform(gfx::Transform()); SetBounds(target_bounds); } layout_inputs_ = new_layout_inputs; delegate_view_->UpdateTranslucentBackground(); // Setting visibility during an animation causes the visibility property to // animate. Set the visibility property without an animation. if (new_layout_inputs.shelf_view_opacity != 0.0f && new_layout_inputs.is_active_session_state) { ShowInactive(); } } void HotseatWidget::UpdateTargetBoundsForGesture(int shelf_position) { if (shelf_->IsHorizontalAlignment()) target_bounds_.set_y(shelf_position); else target_bounds_.set_x(shelf_position); } gfx::Size HotseatWidget::GetTranslucentBackgroundSize() const { DCHECK(scrollable_shelf_view_); return scrollable_shelf_view_->GetHotseatBackgroundBounds().size(); } void HotseatWidget::SetFocusCycler(FocusCycler* focus_cycler) { delegate_view_->set_focus_cycler(focus_cycler); if (focus_cycler) focus_cycler->AddWidget(this); } ShelfView* HotseatWidget::GetShelfView() { DCHECK(scrollable_shelf_view_); return scrollable_shelf_view_->shelf_view(); } int HotseatWidget::GetHotseatSize() const { return ShelfConfig::Get()->GetShelfButtonSize(target_hotseat_density_); } int HotseatWidget::GetHotseatFullDragAmount() const { ShelfConfig* shelf_config = ShelfConfig::Get(); return shelf_config->shelf_size() + shelf_config->hotseat_bottom_padding() + GetHotseatSize(); } bool HotseatWidget::UpdateTargetHotseatDensityIfNeeded() { if (CalculateTargetHotseatDensity() == target_hotseat_density_) { return false; } shelf_->shelf_layout_manager()->LayoutShelf(/*animate=*/true); return true; } int HotseatWidget::GetHotseatBackgroundBlurForTest() const { return delegate_view_->background_blur(); } bool HotseatWidget::GetIsTranslucentBackgroundVisibleForTest() const { return delegate_view_->is_translucent_background_visible_for_test(); } bool HotseatWidget::IsShowingShelfMenu() const { return GetShelfView()->IsShowingMenu(); } bool HotseatWidget::EventTargetsShelfView(const ui::LocatedEvent& event) const { DCHECK_EQ(event.target(), GetNativeWindow()); gfx::Point location_in_shelf_view = event.location(); views::View::ConvertPointFromWidget(scrollable_shelf_view_, &location_in_shelf_view); return scrollable_shelf_view_->GetHotseatBackgroundBounds().Contains( location_in_shelf_view); } const ShelfView* HotseatWidget::GetShelfView() const { return const_cast( const_cast(this)->GetShelfView()); } metrics_util::ReportCallback HotseatWidget::GetTranslucentBackgroundReportCallback() { return shelf_->GetTranslucentBackgroundReportCallback(state_); } void HotseatWidget::SetState(HotseatState state) { if (state_ == state) return; state_ = state; // If the hotseat is not extended we can use the normal targeting as the // hidden parts of the hotseat will not block non-shelf items from taking if (state == HotseatState::kExtended) { hotseat_window_targeter_ = std::make_unique( GetNativeWindow(), std::make_unique(this)); } else { hotseat_window_targeter_.reset(); } } ui::Layer* HotseatWidget::GetLayerForNudgeAnimation() { return delegate_view_->layer(); } HotseatWidget::LayoutInputs HotseatWidget::GetLayoutInputs() const { const ShelfLayoutManager* layout_manager = shelf_->shelf_layout_manager(); return {target_bounds_, CalculateShelfViewOpacity(), layout_manager->is_active_session_state()}; } void HotseatWidget::MaybeAdjustTargetBoundsForAppScaling( HotseatState hotseat_target_state) { // Return early if app scaling state does not change. HotseatDensity new_target_hotseat_density = CalculateTargetHotseatDensity(); if (new_target_hotseat_density == target_hotseat_density_) return; target_hotseat_density_ = new_target_hotseat_density; // Update app icons of shelf view. scrollable_shelf_view_->shelf_view()->OnShelfConfigUpdated(); const gfx::Point adjusted_hotseat_origin = gfx::Point( target_bounds_.x(), CalculateHotseatYInScreen(hotseat_target_state)); target_bounds_ = gfx::Rect(adjusted_hotseat_origin, gfx::Size(target_bounds_.width(), GetHotseatSize())); } HotseatDensity HotseatWidget::CalculateTargetHotseatDensity() const { // App scaling is only applied to the standard shelf. So the hotseat density // should not update in dense shelf. if (ShelfConfig::Get()->is_dense()) return target_hotseat_density_; // TODO(crbug.com/1081476): Currently the scaling animation of hotseat bounds // and that of shelf icons do not synchronize due to performance issue. As a // result, shelf scaling is not applied to the hotseat state transition, such // as the transition from the home launcher state to the extended state. // Hotseat density relies on the hotseat bounds in the home launcher state // instead of the current hotseat state. // Try candidate button sizes in decreasing order. If shelf buttons in one // size can show without scrolling, return the density type corresponding to // that particular size; if no candidate size can make it, return // HotseatDensity::kDense. const std::vector kCandidates = {HotseatDensity::kNormal, HotseatDensity::kSemiDense}; for (const auto& candidate : kCandidates) { if (!scrollable_shelf_view_->RequiresScrollingForItemSize( target_size_for_shown_state_, ShelfConfig::Get()->GetShelfButtonSize(candidate))) { return candidate; } } return HotseatDensity::kDense; } void HotseatWidget::LayoutHotseatByAnimation(double target_opacity, const gfx::Rect& target_bounds) { ui::Layer* hotseat_layer = GetNativeView()->layer(); // Bounds animations do not take transforms into account, but animations // between hidden and extended state use transform to animate. Clear any // transform that may have been set by the previous animation, and update // current bounds to match it. gfx::RectF current_bounds_f(GetNativeView()->GetBoundsInScreen()); hotseat_layer->transform().TransformRect(¤t_bounds_f); gfx::Rect current_bounds = gfx::ToEnclosingRect(current_bounds_f); // If the bounds size has not changed, set the target bounds immediately, and // animate using transform. // Avoid transforms if the transition has a horizontal component - logic for // setting scrollable shelf padding depends on the horizontal in screen // bounds, and setting horizontal translation would interfere with that. Note // that generally, if horizontal widget position changes, so will its width. const bool animate_transform = target_bounds.size() == current_bounds.size() && target_bounds.x() == current_bounds.x(); if (animate_transform) { SetBounds(target_bounds); gfx::Transform initial_transform; initial_transform.Translate(current_bounds.origin() - target_bounds.origin()); hotseat_layer->SetTransform(initial_transform); } else { hotseat_layer->SetTransform(gfx::Transform()); SetBounds(current_bounds); } ui::ScopedLayerAnimationSettings animation_setter( hotseat_layer->GetAnimator()); animation_setter.SetTransitionDuration( ShelfConfig::Get()->shelf_animation_duration()); animation_setter.SetTweenType(gfx::Tween::EASE_OUT); animation_setter.SetPreemptionStrategy( ui::LayerAnimator::IMMEDIATELY_ANIMATE_TO_NEW_TARGET); absl::optional reporter; if (state_ != HotseatState::kNone) { reporter.emplace(animation_setter.GetAnimator(), shelf_->GetHotseatTransitionReportCallback(state_)); } if (animate_transform) { hotseat_layer->SetOpacity(target_opacity); hotseat_layer->SetTransform(gfx::Transform()); return; } if (!state_transition_in_progress_.has_value()) { // Hotseat animation is not triggered by the update in |state_|. So apply // the normal bounds animation. StartNormalBoundsAnimation(target_opacity, target_bounds); return; } if (HasSpecialAnimation(*state_transition_in_progress_)) { StartHotseatTransitionAnimation(*state_transition_in_progress_, target_opacity, target_bounds); } else { StartNormalBoundsAnimation(target_opacity, target_bounds); } } void HotseatWidget::StartHotseatTransitionAnimation( StateTransition state_transition, double target_opacity, const gfx::Rect& target_bounds) { ui::Layer* hotseat_layer = GetNativeView()->layer(); std::unique_ptr animation_elements; switch (state_transition) { case StateTransition::kHomeLauncherAndExtended: animation_elements = std::make_unique( target_bounds, target_opacity, hotseat_layer, /*hotseat_widget=*/this); break; case StateTransition::kHomeLauncherAndHidden: animation_elements = std::make_unique( target_bounds, target_opacity, hotseat_layer, /*hotseat_widget=*/this); break; case StateTransition::kHiddenAndExtended: case StateTransition::kOther: NOTREACHED(); } auto* sequence = new ui::LayerAnimationSequence(std::move(animation_elements)); hotseat_layer->GetAnimator()->StartAnimation(sequence); } void HotseatWidget::StartNormalBoundsAnimation(double target_opacity, const gfx::Rect& target_bounds) { GetNativeView()->layer()->SetOpacity(target_opacity); SetBounds(target_bounds); } } // namespace ash