#include "chromeos/ui/frame/highlight_border_overlay.h"
#include <algorithm>
#include <map>
#include "base/check.h"
#include "base/memory/raw_ptr.h"
#include "chromeos/ui/base/chromeos_ui_constants.h"
#include "chromeos/ui/base/window_properties.h"
#include "chromeos/ui/base/window_state_type.h"
#include "chromeos/ui/frame/frame_utils.h"
#include "chromeos/ui/frame/highlight_border_overlay_delegate.h"
#include "third_party/skia/include/core/SkColor.h"
#include "ui/aura/client/aura_constants.h"
#include "ui/aura/window.h"
#include "ui/display/screen.h"
#include "ui/gfx/geometry/size.h"
#include "ui/gfx/image/canvas_image_source.h"
#include "ui/gfx/image/image_skia.h"
#include "ui/views/highlight_border.h"
#include "ui/views/widget/widget.h"
namespace {
constexpr size_t kMaxImageSourceNum = 6;
constexpr views::HighlightBorder::Type kBorderType =
views::HighlightBorder::Type::kHighlightBorderOnShadow;
struct HighlightBorderProperties {
bool operator<(const HighlightBorderProperties& other) const {
return std::tie(highlight_color, border_color, corner_radius) <
std::tie(other.highlight_color, other.border_color,
other.corner_radius);
}
SkColor highlight_color = SK_ColorTRANSPARENT;
SkColor border_color = SK_ColorTRANSPARENT;
int corner_radius = 0;
};
class HighlightBorderImageSource : public gfx::CanvasImageSource {
public:
HighlightBorderImageSource(const gfx::Size& size,
const HighlightBorderProperties& properties)
: gfx::CanvasImageSource(size), properties_(properties) {}
HighlightBorderImageSource(const HighlightBorderImageSource&) = delete;
HighlightBorderImageSource& operator=(const HighlightBorderImageSource&) =
delete;
~HighlightBorderImageSource() override = default;
void Draw(gfx::Canvas* canvas) override {
views::HighlightBorder::PaintBorderToCanvas(
canvas, properties_.highlight_color, properties_.border_color,
gfx::Rect(size()), gfx::RoundedCornersF(properties_.corner_radius),
kBorderType);
}
private:
const HighlightBorderProperties properties_;
};
gfx::ImageSkia GetHighlightBorderImageMatching(
const gfx::Size& size,
const HighlightBorderProperties& properties) {
static base::NoDestructor<std::map<HighlightBorderProperties, gfx::ImageSkia>>
image_source_cache;
auto iter = image_source_cache->find(properties);
if (iter != image_source_cache->end()) {
return iter->second;
}
std::erase_if(*image_source_cache, [](auto& key_and_image_source) {
return key_and_image_source.second.IsUniquelyOwned();
});
const auto& [iterator, inserted] = image_source_cache->emplace(
properties, gfx::ImageSkia(std::make_unique<HighlightBorderImageSource>(
size, properties),
1.0f));
DCHECK(inserted);
DCHECK_LE(image_source_cache->size(), kMaxImageSourceNum);
return iterator->second;
}
int GetCornerRadius(const aura::Window* window,
HighlightBorderOverlayDelegate* delegate) {
auto window_radii =
window->GetProperty(aura::client::kWindowRoundedCornersKey);
const bool should_rounded_border =
delegate ? delegate->ShouldRoundHighlightBorderForWindow(window) : true;
if (window_radii && should_rounded_border) {
return window_radii->upper_left();
}
return 0;
}
}
HighlightBorderOverlay::HighlightBorderOverlay(
views::Widget* widget,
std::unique_ptr<HighlightBorderOverlayDelegate> delegate)
: layer_(ui::LAYER_NINE_PATCH),
widget_(widget),
window_(widget->GetNativeWindow()),
delegate_(std::move(delegate)) {
rounded_corner_radius_ = GetCornerRadius(window_, delegate_.get());
layer_.SetName("HighlightBorderOverlay");
layer_.SetFillsBoundsOpaquely(false);
UpdateNinePatchLayer();
UpdateLayerVisibilityAndBounds();
window_->AddObserver(this);
auto* widget_layer = widget_->GetLayer();
widget_layer->Add(&layer_);
widget_layer->StackAtTop(&layer_);
}
HighlightBorderOverlay::~HighlightBorderOverlay() {
if (window_)
window_->RemoveObserver(this);
}
gfx::Size HighlightBorderOverlay::CalculateImageSourceSize() const {
gfx::Rect image_source_bounds(1, 1);
image_source_bounds.Inset(-CalculateBorderRegion());
return image_source_bounds.size();
}
void HighlightBorderOverlay::OnWindowBoundsChanged(
aura::Window* window,
const gfx::Rect& old_bounds,
const gfx::Rect& new_bounds,
ui::PropertyChangeReason reason) {
UpdateLayerVisibilityAndBounds();
}
void HighlightBorderOverlay::OnWindowPropertyChanged(aura::Window* window,
const void* key,
intptr_t old) {
if (key == chromeos::kFrameActiveColorKey) {
if (window->GetProperty(chromeos::kFrameActiveColorKey) !=
static_cast<SkColor>(old)) {
UpdateNinePatchLayer();
}
return;
}
if (chromeos::CanPropertyEffectWindowRoundedCorners(key)) {
const int corner_radius = GetCornerRadius(window_, delegate_.get());
if (rounded_corner_radius_ != corner_radius) {
rounded_corner_radius_ = corner_radius;
UpdateNinePatchLayer();
}
return;
}
if (key == chromeos::kWindowStateTypeKey) {
UpdateLayerVisibilityAndBounds();
return;
}
}
void HighlightBorderOverlay::OnWindowDestroying(aura::Window* window) {
DCHECK_EQ(window_, window);
window_->RemoveObserver(this);
window_ = nullptr;
}
void HighlightBorderOverlay::OnDisplayTabletStateChanged(
display::TabletState state) {
UpdateLayerVisibilityAndBounds();
}
gfx::Insets HighlightBorderOverlay::CalculateBorderRegion() const {
return gfx::Insets(2 * views::kHighlightBorderThickness +
rounded_corner_radius_);
}
void HighlightBorderOverlay::UpdateLayerVisibilityAndBounds() {
gfx::Rect layer_bounds(widget_->GetWindowBoundsInScreen().size());
layer_bounds.Inset(-gfx::Insets(views::kHighlightBorderThickness));
const bool in_tablet_mode = display::Screen::Get()->InTabletMode();
const auto window_state_type =
window_->GetProperty(chromeos::kWindowStateTypeKey);
const bool show_border_in_tablet_mode =
in_tablet_mode &&
window_state_type == chromeos::WindowStateType::kFloated &&
window_state_type == chromeos::WindowStateType::kPip;
const bool show_border_in_clamshell_mode =
!in_tablet_mode &&
window_state_type != chromeos::WindowStateType::kFullscreen;
const gfx::Insets border_region = CalculateBorderRegion();
const bool fits_layer_bounds =
border_region.width() <= layer_bounds.width() &&
border_region.height() <= layer_bounds.height();
const bool show_border =
fits_layer_bounds &&
(show_border_in_clamshell_mode || show_border_in_tablet_mode);
if (!show_border) {
layer_.SetVisible(false);
return;
}
layer_.SetVisible(true);
if (layer_bounds != layer_.bounds())
layer_.SetBounds(layer_bounds);
}
void HighlightBorderOverlay::UpdateNinePatchLayer() {
const gfx::Size image_source_size = CalculateImageSourceSize();
const views::View& view = *(widget_->GetContentsView());
SkColor highlight_color =
views::HighlightBorder::GetHighlightColor(view, kBorderType);
SkColor border_color =
views::HighlightBorder::GetBorderColor(view, kBorderType);
const HighlightBorderProperties properties(highlight_color, border_color,
rounded_corner_radius_);
layer_.UpdateNinePatchLayerImage(
GetHighlightBorderImageMatching(image_source_size, properties));
gfx::Rect aperture(image_source_size);
gfx::Insets border_region = CalculateBorderRegion();
aperture.Inset(border_region);
layer_.UpdateNinePatchLayerAperture(aperture);
layer_.UpdateNinePatchLayerBorder(
gfx::Rect(border_region.left(), border_region.top(),
border_region.width(), border_region.height()));
}