client_frame_view_linux.cc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507
  1. // Copyright (c) 2021 Ryan Gonzalez.
  2. // Use of this source code is governed by the MIT license that can be
  3. // found in the LICENSE file.
  4. #include "shell/browser/ui/views/client_frame_view_linux.h"
  5. #include <algorithm>
  6. #include "base/strings/utf_string_conversions.h"
  7. #include "cc/paint/paint_filter.h"
  8. #include "cc/paint/paint_flags.h"
  9. #include "shell/browser/native_window_views.h"
  10. #include "shell/browser/ui/electron_desktop_window_tree_host_linux.h"
  11. #include "shell/browser/ui/views/frameless_view.h"
  12. #include "ui/base/hit_test.h"
  13. #include "ui/base/l10n/l10n_util.h"
  14. #include "ui/base/metadata/metadata_impl_macros.h"
  15. #include "ui/base/models/image_model.h"
  16. #include "ui/gfx/canvas.h"
  17. #include "ui/gfx/font_list.h"
  18. #include "ui/gfx/geometry/insets.h"
  19. #include "ui/gfx/geometry/rect.h"
  20. #include "ui/gfx/geometry/skia_conversions.h"
  21. #include "ui/gfx/text_constants.h"
  22. #include "ui/gtk/gtk_compat.h" // nogncheck
  23. #include "ui/gtk/gtk_util.h" // nogncheck
  24. #include "ui/linux/linux_ui.h"
  25. #include "ui/linux/nav_button_provider.h"
  26. #include "ui/native_theme/native_theme.h"
  27. #include "ui/strings/grit/ui_strings.h"
  28. #include "ui/views/controls/button/image_button.h"
  29. #include "ui/views/style/typography.h"
  30. #include "ui/views/widget/widget.h"
  31. #include "ui/views/window/frame_buttons.h"
  32. #include "ui/views/window/window_button_order_provider.h"
  33. namespace electron {
  34. namespace {
  35. // These values should be the same as Chromium uses.
  36. constexpr int kResizeOutsideBorderSize = 10;
  37. constexpr int kResizeInsideBoundsSize = 5;
  38. ui::NavButtonProvider::ButtonState ButtonStateToNavButtonProviderState(
  39. views::Button::ButtonState state) {
  40. switch (state) {
  41. case views::Button::STATE_NORMAL:
  42. return ui::NavButtonProvider::ButtonState::kNormal;
  43. case views::Button::STATE_HOVERED:
  44. return ui::NavButtonProvider::ButtonState::kHovered;
  45. case views::Button::STATE_PRESSED:
  46. return ui::NavButtonProvider::ButtonState::kPressed;
  47. case views::Button::STATE_DISABLED:
  48. return ui::NavButtonProvider::ButtonState::kDisabled;
  49. case views::Button::STATE_COUNT:
  50. default:
  51. NOTREACHED();
  52. }
  53. }
  54. } // namespace
  55. ClientFrameViewLinux::ClientFrameViewLinux()
  56. : theme_(ui::NativeTheme::GetInstanceForNativeUi()),
  57. nav_button_provider_(
  58. ui::LinuxUiTheme::GetForProfile(nullptr)->CreateNavButtonProvider()),
  59. nav_buttons_{
  60. NavButton{ui::NavButtonProvider::FrameButtonDisplayType::kClose,
  61. views::FrameButton::kClose, &views::Widget::Close,
  62. IDS_APP_ACCNAME_CLOSE, HTCLOSE},
  63. NavButton{ui::NavButtonProvider::FrameButtonDisplayType::kMaximize,
  64. views::FrameButton::kMaximize, &views::Widget::Maximize,
  65. IDS_APP_ACCNAME_MAXIMIZE, HTMAXBUTTON},
  66. NavButton{ui::NavButtonProvider::FrameButtonDisplayType::kRestore,
  67. views::FrameButton::kMaximize, &views::Widget::Restore,
  68. IDS_APP_ACCNAME_RESTORE, HTMAXBUTTON},
  69. NavButton{ui::NavButtonProvider::FrameButtonDisplayType::kMinimize,
  70. views::FrameButton::kMinimize, &views::Widget::Minimize,
  71. IDS_APP_ACCNAME_MINIMIZE, HTMINBUTTON},
  72. },
  73. trailing_frame_buttons_{views::FrameButton::kMinimize,
  74. views::FrameButton::kMaximize,
  75. views::FrameButton::kClose} {
  76. for (auto& button : nav_buttons_) {
  77. button.button = new views::ImageButton();
  78. button.button->SetImageVerticalAlignment(views::ImageButton::ALIGN_MIDDLE);
  79. button.button->SetAccessibleName(
  80. l10n_util::GetStringUTF16(button.accessibility_id));
  81. AddChildView(button.button);
  82. }
  83. title_ = new views::Label();
  84. title_->SetSubpixelRenderingEnabled(false);
  85. title_->SetAutoColorReadabilityEnabled(false);
  86. title_->SetHorizontalAlignment(gfx::ALIGN_CENTER);
  87. title_->SetVerticalAlignment(gfx::ALIGN_MIDDLE);
  88. title_->SetTextStyle(views::style::STYLE_TAB_ACTIVE);
  89. AddChildView(title_);
  90. native_theme_observer_.Observe(theme_);
  91. if (auto* ui = ui::LinuxUi::instance()) {
  92. ui->AddWindowButtonOrderObserver(this);
  93. OnWindowButtonOrderingChange();
  94. }
  95. }
  96. ClientFrameViewLinux::~ClientFrameViewLinux() {
  97. if (auto* ui = ui::LinuxUi::instance())
  98. ui->RemoveWindowButtonOrderObserver(this);
  99. theme_->RemoveObserver(this);
  100. }
  101. void ClientFrameViewLinux::Init(NativeWindowViews* window,
  102. views::Widget* frame) {
  103. FramelessView::Init(window, frame);
  104. // Unretained() is safe because the subscription is saved into an instance
  105. // member and thus will be cancelled upon the instance's destruction.
  106. paint_as_active_changed_subscription_ =
  107. frame_->RegisterPaintAsActiveChangedCallback(base::BindRepeating(
  108. &ClientFrameViewLinux::PaintAsActiveChanged, base::Unretained(this)));
  109. auto* tree_host = static_cast<ElectronDesktopWindowTreeHostLinux*>(
  110. ElectronDesktopWindowTreeHostLinux::GetHostForWidget(
  111. window->GetAcceleratedWidget()));
  112. host_supports_client_frame_shadow_ = tree_host->SupportsClientFrameShadow();
  113. UpdateWindowTitle();
  114. for (auto& button : nav_buttons_) {
  115. // Unretained() is safe because the buttons are added as children to, and
  116. // thus owned by, this view. Thus, the buttons themselves will be destroyed
  117. // when this view is destroyed, and the frame's life must never outlive the
  118. // view.
  119. button.button->SetCallback(
  120. base::BindRepeating(button.callback, base::Unretained(frame)));
  121. }
  122. UpdateThemeValues();
  123. }
  124. gfx::Insets ClientFrameViewLinux::GetBorderDecorationInsets() const {
  125. const auto insets = GetFrameProvider()->GetFrameThicknessDip();
  126. // We shouldn't draw frame decorations for the tiled edges.
  127. // See https://wayland.app/protocols/xdg-shell#xdg_toplevel:enum:state
  128. const auto& edges = tiled_edges();
  129. return gfx::Insets::TLBR(
  130. edges.top ? 0 : insets.top(), edges.left ? 0 : insets.left(),
  131. edges.bottom ? 0 : insets.bottom(), edges.right ? 0 : insets.right());
  132. }
  133. gfx::Insets ClientFrameViewLinux::GetInputInsets() const {
  134. return gfx::Insets{
  135. host_supports_client_frame_shadow_ ? -kResizeOutsideBorderSize : 0};
  136. }
  137. gfx::Rect ClientFrameViewLinux::GetWindowContentBounds() const {
  138. gfx::Rect content_bounds = bounds();
  139. content_bounds.Inset(GetBorderDecorationInsets());
  140. return content_bounds;
  141. }
  142. SkRRect ClientFrameViewLinux::GetRoundedWindowContentBounds() const {
  143. SkRect rect = gfx::RectToSkRect(GetWindowContentBounds());
  144. SkRRect rrect;
  145. if (!frame_->IsMaximized()) {
  146. SkPoint round_point{theme_values_.window_border_radius,
  147. theme_values_.window_border_radius};
  148. SkPoint radii[] = {round_point, round_point, {}, {}};
  149. rrect.setRectRadii(rect, radii);
  150. } else {
  151. rrect.setRect(rect);
  152. }
  153. return rrect;
  154. }
  155. void ClientFrameViewLinux::OnNativeThemeUpdated(
  156. ui::NativeTheme* observed_theme) {
  157. UpdateThemeValues();
  158. }
  159. void ClientFrameViewLinux::OnWindowButtonOrderingChange() {
  160. auto* provider = views::WindowButtonOrderProvider::GetInstance();
  161. leading_frame_buttons_ = provider->leading_buttons();
  162. trailing_frame_buttons_ = provider->trailing_buttons();
  163. InvalidateLayout();
  164. }
  165. int ClientFrameViewLinux::ResizingBorderHitTest(const gfx::Point& point) {
  166. return ResizingBorderHitTestImpl(
  167. point,
  168. GetBorderDecorationInsets() + gfx::Insets(kResizeInsideBoundsSize));
  169. }
  170. gfx::Rect ClientFrameViewLinux::GetBoundsForClientView() const {
  171. gfx::Rect client_bounds = bounds();
  172. if (!frame_->IsFullscreen()) {
  173. client_bounds.Inset(GetBorderDecorationInsets());
  174. client_bounds.Inset(
  175. gfx::Insets::TLBR(GetTitlebarBounds().height(), 0, 0, 0));
  176. }
  177. return client_bounds;
  178. }
  179. gfx::Rect ClientFrameViewLinux::GetWindowBoundsForClientBounds(
  180. const gfx::Rect& client_bounds) const {
  181. gfx::Insets insets = bounds().InsetsFrom(GetBoundsForClientView());
  182. return gfx::Rect(std::max(0, client_bounds.x() - insets.left()),
  183. std::max(0, client_bounds.y() - insets.top()),
  184. client_bounds.width() + insets.width(),
  185. client_bounds.height() + insets.height());
  186. }
  187. int ClientFrameViewLinux::NonClientHitTest(const gfx::Point& point) {
  188. int component = ResizingBorderHitTest(point);
  189. if (component != HTNOWHERE) {
  190. return component;
  191. }
  192. for (auto& button : nav_buttons_) {
  193. if (button.button->GetVisible() &&
  194. button.button->GetMirroredBounds().Contains(point)) {
  195. return button.hit_test_id;
  196. }
  197. }
  198. if (GetTitlebarBounds().Contains(point)) {
  199. return HTCAPTION;
  200. }
  201. return FramelessView::NonClientHitTest(point);
  202. }
  203. ui::WindowFrameProvider* ClientFrameViewLinux::GetFrameProvider() const {
  204. const bool tiled = tiled_edges().top || tiled_edges().left ||
  205. tiled_edges().bottom || tiled_edges().right;
  206. return ui::LinuxUiTheme::GetForProfile(nullptr)->GetWindowFrameProvider(
  207. !host_supports_client_frame_shadow_, tiled, frame_->IsMaximized());
  208. }
  209. void ClientFrameViewLinux::GetWindowMask(const gfx::Size& size,
  210. SkPath* window_mask) {
  211. // Nothing to do here, as transparency is used for decorations, not masks.
  212. }
  213. void ClientFrameViewLinux::UpdateWindowTitle() {
  214. title_->SetText(base::UTF8ToUTF16(window_->GetTitle()));
  215. }
  216. void ClientFrameViewLinux::SizeConstraintsChanged() {
  217. InvalidateLayout();
  218. }
  219. gfx::Size ClientFrameViewLinux::CalculatePreferredSize(
  220. const views::SizeBounds& available_size) const {
  221. return SizeWithDecorations(
  222. FramelessView::CalculatePreferredSize(available_size));
  223. }
  224. gfx::Size ClientFrameViewLinux::GetMinimumSize() const {
  225. return SizeWithDecorations(FramelessView::GetMinimumSize());
  226. }
  227. gfx::Size ClientFrameViewLinux::GetMaximumSize() const {
  228. return SizeWithDecorations(FramelessView::GetMaximumSize());
  229. }
  230. void ClientFrameViewLinux::Layout(PassKey) {
  231. LayoutSuperclass<FramelessView>(this);
  232. if (frame_->IsFullscreen()) {
  233. // Just hide everything and return.
  234. for (NavButton& button : nav_buttons_) {
  235. button.button->SetVisible(false);
  236. }
  237. title_->SetVisible(false);
  238. return;
  239. }
  240. UpdateButtonImages();
  241. LayoutButtons();
  242. gfx::Rect title_bounds(GetTitlebarContentBounds());
  243. title_bounds.Inset(theme_values_.title_padding);
  244. title_->SetVisible(true);
  245. title_->SetBounds(title_bounds.x(), title_bounds.y(), title_bounds.width(),
  246. title_bounds.height());
  247. }
  248. void ClientFrameViewLinux::OnPaint(gfx::Canvas* canvas) {
  249. if (!frame_->IsFullscreen()) {
  250. GetFrameProvider()->PaintWindowFrame(
  251. canvas, GetLocalBounds(), GetTitlebarBounds().bottom(),
  252. ShouldPaintAsActive(), GetInputInsets());
  253. }
  254. }
  255. void ClientFrameViewLinux::PaintAsActiveChanged() {
  256. UpdateThemeValues();
  257. }
  258. void ClientFrameViewLinux::UpdateThemeValues() {
  259. gtk::GtkCssContext window_context =
  260. gtk::AppendCssNodeToStyleContext({}, "window.background.csd");
  261. gtk::GtkCssContext headerbar_context = gtk::AppendCssNodeToStyleContext(
  262. {}, "headerbar.default-decoration.titlebar");
  263. gtk::GtkCssContext title_context =
  264. gtk::AppendCssNodeToStyleContext(headerbar_context, "label.title");
  265. gtk::GtkCssContext button_context = gtk::AppendCssNodeToStyleContext(
  266. headerbar_context, "button.image-button");
  267. gtk_style_context_set_parent(headerbar_context, window_context);
  268. gtk_style_context_set_parent(title_context, headerbar_context);
  269. gtk_style_context_set_parent(button_context, headerbar_context);
  270. // ShouldPaintAsActive asks the widget, so assume active if the widget is not
  271. // set yet.
  272. if (GetWidget() != nullptr && !ShouldPaintAsActive()) {
  273. gtk_style_context_set_state(window_context, GTK_STATE_FLAG_BACKDROP);
  274. gtk_style_context_set_state(headerbar_context, GTK_STATE_FLAG_BACKDROP);
  275. gtk_style_context_set_state(title_context, GTK_STATE_FLAG_BACKDROP);
  276. gtk_style_context_set_state(button_context, GTK_STATE_FLAG_BACKDROP);
  277. }
  278. theme_values_.window_border_radius =
  279. GetFrameProvider()->GetTopCornerRadiusDip();
  280. gtk::GtkStyleContextGet(headerbar_context, "min-height",
  281. &theme_values_.titlebar_min_height, nullptr);
  282. theme_values_.titlebar_padding =
  283. gtk::GtkStyleContextGetPadding(headerbar_context);
  284. theme_values_.title_color = gtk::GtkStyleContextGetColor(title_context);
  285. theme_values_.title_padding = gtk::GtkStyleContextGetPadding(title_context);
  286. gtk::GtkStyleContextGet(button_context, "min-height",
  287. &theme_values_.button_min_size, nullptr);
  288. theme_values_.button_padding = gtk::GtkStyleContextGetPadding(button_context);
  289. title_->SetEnabledColor(theme_values_.title_color);
  290. InvalidateLayout();
  291. SchedulePaint();
  292. }
  293. ui::NavButtonProvider::FrameButtonDisplayType
  294. ClientFrameViewLinux::GetButtonTypeToSkip() const {
  295. return frame_->IsMaximized()
  296. ? ui::NavButtonProvider::FrameButtonDisplayType::kMaximize
  297. : ui::NavButtonProvider::FrameButtonDisplayType::kRestore;
  298. }
  299. void ClientFrameViewLinux::UpdateButtonImages() {
  300. nav_button_provider_->RedrawImages(theme_values_.button_min_size,
  301. frame_->IsMaximized(),
  302. ShouldPaintAsActive());
  303. ui::NavButtonProvider::FrameButtonDisplayType skip_type =
  304. GetButtonTypeToSkip();
  305. for (NavButton& button : nav_buttons_) {
  306. if (button.type == skip_type) {
  307. continue;
  308. }
  309. for (size_t state_id = 0; state_id < views::Button::STATE_COUNT;
  310. state_id++) {
  311. views::Button::ButtonState state =
  312. static_cast<views::Button::ButtonState>(state_id);
  313. button.button->SetImageModel(
  314. state, ui::ImageModel::FromImageSkia(nav_button_provider_->GetImage(
  315. button.type, ButtonStateToNavButtonProviderState(state))));
  316. }
  317. }
  318. }
  319. void ClientFrameViewLinux::LayoutButtons() {
  320. for (NavButton& button : nav_buttons_) {
  321. button.button->SetVisible(false);
  322. }
  323. gfx::Rect remaining_content_bounds = GetTitlebarContentBounds();
  324. LayoutButtonsOnSide(ButtonSide::kLeading, &remaining_content_bounds);
  325. LayoutButtonsOnSide(ButtonSide::kTrailing, &remaining_content_bounds);
  326. }
  327. void ClientFrameViewLinux::LayoutButtonsOnSide(
  328. ButtonSide side,
  329. gfx::Rect* remaining_content_bounds) {
  330. ui::NavButtonProvider::FrameButtonDisplayType skip_type =
  331. GetButtonTypeToSkip();
  332. std::vector<views::FrameButton> frame_buttons;
  333. switch (side) {
  334. case ButtonSide::kLeading:
  335. frame_buttons = leading_frame_buttons_;
  336. break;
  337. case ButtonSide::kTrailing:
  338. frame_buttons = trailing_frame_buttons_;
  339. // We always lay buttons out going from the edge towards the center, but
  340. // they are given to us as left-to-right, so reverse them.
  341. std::ranges::reverse(frame_buttons);
  342. break;
  343. default:
  344. NOTREACHED();
  345. }
  346. for (views::FrameButton frame_button : frame_buttons) {
  347. auto button =
  348. std::ranges::find_if(nav_buttons_, [&](const NavButton& test) {
  349. return test.type != skip_type && test.frame_button == frame_button;
  350. });
  351. CHECK(button != nav_buttons_.end())
  352. << "Failed to find frame button: " << static_cast<int>(frame_button);
  353. if (button->type == skip_type) {
  354. continue;
  355. }
  356. button->button->SetVisible(true);
  357. int button_width = theme_values_.button_min_size;
  358. int next_button_offset =
  359. button_width + nav_button_provider_->GetInterNavButtonSpacing();
  360. int x_position = 0;
  361. gfx::Insets inset_after_placement;
  362. switch (side) {
  363. case ButtonSide::kLeading:
  364. x_position = remaining_content_bounds->x();
  365. inset_after_placement.set_left(next_button_offset);
  366. break;
  367. case ButtonSide::kTrailing:
  368. x_position = remaining_content_bounds->right() - button_width;
  369. inset_after_placement.set_right(next_button_offset);
  370. break;
  371. default:
  372. NOTREACHED();
  373. }
  374. button->button->SetBounds(x_position, remaining_content_bounds->y(),
  375. button_width, remaining_content_bounds->height());
  376. remaining_content_bounds->Inset(inset_after_placement);
  377. }
  378. }
  379. gfx::Rect ClientFrameViewLinux::GetTitlebarBounds() const {
  380. if (frame_->IsFullscreen()) {
  381. return {};
  382. }
  383. int font_height = gfx::FontList().GetHeight();
  384. int titlebar_height =
  385. std::max(font_height, theme_values_.titlebar_min_height) +
  386. GetTitlebarContentInsets().height();
  387. gfx::Insets decoration_insets = GetBorderDecorationInsets();
  388. // We add the inset height here, so the .Inset() that follows won't reduce it
  389. // to be too small.
  390. gfx::Rect titlebar(width(), titlebar_height + decoration_insets.height());
  391. titlebar.Inset(decoration_insets);
  392. return titlebar;
  393. }
  394. gfx::Insets ClientFrameViewLinux::GetTitlebarContentInsets() const {
  395. return theme_values_.titlebar_padding +
  396. nav_button_provider_->GetTopAreaSpacing();
  397. }
  398. gfx::Rect ClientFrameViewLinux::GetTitlebarContentBounds() const {
  399. gfx::Rect titlebar(GetTitlebarBounds());
  400. titlebar.Inset(GetTitlebarContentInsets());
  401. return titlebar;
  402. }
  403. gfx::Size ClientFrameViewLinux::SizeWithDecorations(gfx::Size size) const {
  404. gfx::Insets decoration_insets = GetBorderDecorationInsets();
  405. size.Enlarge(0, GetTitlebarBounds().height());
  406. size.Enlarge(decoration_insets.width(), decoration_insets.height());
  407. return size;
  408. }
  409. views::View* ClientFrameViewLinux::TargetForRect(views::View* root,
  410. const gfx::Rect& rect) {
  411. return views::NonClientFrameView::TargetForRect(root, rect);
  412. }
  413. int ClientFrameViewLinux::GetTranslucentTopAreaHeight() const {
  414. return 0;
  415. }
  416. BEGIN_METADATA(ClientFrameViewLinux) END_METADATA
  417. } // namespace electron