nav_button_provider_gtk.cc 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. // Copyright 2017 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. #include "ui/gtk/nav_button_provider_gtk.h"
  5. #include "base/notreached.h"
  6. #include "ui/base/glib/glib_cast.h"
  7. #include "ui/base/glib/scoped_gobject.h"
  8. #include "ui/gfx/image/image_skia.h"
  9. #include "ui/gfx/image/image_skia_rep.h"
  10. #include "ui/gfx/image/image_skia_source.h"
  11. #include "ui/gtk/gtk_compat.h"
  12. #include "ui/gtk/gtk_util.h"
  13. #include "ui/linux/nav_button_provider.h"
  14. #include "ui/views/widget/widget.h"
  15. namespace gtk {
  16. namespace {
  17. struct NavButtonIcon {
  18. // Used on Gtk3.
  19. ScopedGObject<GdkPixbuf> pixbuf;
  20. // Used on Gtk4.
  21. ScopedGObject<GdkTexture> texture;
  22. };
  23. // gtkheaderbar.c uses GTK_ICON_SIZE_MENU, which is 16px.
  24. const int kNavButtonIconSize = 16;
  25. // Specified in GtkHeaderBar spec.
  26. const int kHeaderSpacing = 6;
  27. const char* ButtonStyleClassFromButtonType(
  28. ui::NavButtonProvider::FrameButtonDisplayType type) {
  29. switch (type) {
  30. case ui::NavButtonProvider::FrameButtonDisplayType::kMinimize:
  31. return "minimize";
  32. case ui::NavButtonProvider::FrameButtonDisplayType::kMaximize:
  33. case ui::NavButtonProvider::FrameButtonDisplayType::kRestore:
  34. return "maximize";
  35. case ui::NavButtonProvider::FrameButtonDisplayType::kClose:
  36. return "close";
  37. default:
  38. NOTREACHED();
  39. return "";
  40. }
  41. }
  42. GtkStateFlags GtkStateFlagsFromButtonState(
  43. ui::NavButtonProvider::ButtonState state) {
  44. switch (state) {
  45. case ui::NavButtonProvider::ButtonState::kNormal:
  46. return GTK_STATE_FLAG_NORMAL;
  47. case ui::NavButtonProvider::ButtonState::kHovered:
  48. return GTK_STATE_FLAG_PRELIGHT;
  49. case ui::NavButtonProvider::ButtonState::kPressed:
  50. return static_cast<GtkStateFlags>(GTK_STATE_FLAG_PRELIGHT |
  51. GTK_STATE_FLAG_ACTIVE);
  52. case ui::NavButtonProvider::ButtonState::kDisabled:
  53. return GTK_STATE_FLAG_INSENSITIVE;
  54. default:
  55. NOTREACHED();
  56. return GTK_STATE_FLAG_NORMAL;
  57. }
  58. }
  59. const char* IconNameFromButtonType(
  60. ui::NavButtonProvider::FrameButtonDisplayType type) {
  61. switch (type) {
  62. case ui::NavButtonProvider::FrameButtonDisplayType::kMinimize:
  63. return "window-minimize-symbolic";
  64. case ui::NavButtonProvider::FrameButtonDisplayType::kMaximize:
  65. return "window-maximize-symbolic";
  66. case ui::NavButtonProvider::FrameButtonDisplayType::kRestore:
  67. return "window-restore-symbolic";
  68. case ui::NavButtonProvider::FrameButtonDisplayType::kClose:
  69. return "window-close-symbolic";
  70. default:
  71. NOTREACHED();
  72. return "";
  73. }
  74. }
  75. gfx::Size LoadNavButtonIcon(ui::NavButtonProvider::FrameButtonDisplayType type,
  76. GtkStyleContext* button_context,
  77. int scale,
  78. NavButtonIcon* icon = nullptr) {
  79. const char* icon_name = IconNameFromButtonType(type);
  80. if (!GtkCheckVersion(4)) {
  81. auto icon_info = TakeGObject(gtk_icon_theme_lookup_icon_for_scale(
  82. GetDefaultIconTheme(), icon_name, kNavButtonIconSize, scale,
  83. static_cast<GtkIconLookupFlags>(GTK_ICON_LOOKUP_USE_BUILTIN |
  84. GTK_ICON_LOOKUP_GENERIC_FALLBACK)));
  85. auto icon_pixbuf = TakeGObject(gtk_icon_info_load_symbolic_for_context(
  86. icon_info, button_context, nullptr, nullptr));
  87. gfx::Size size{gdk_pixbuf_get_width(icon_pixbuf),
  88. gdk_pixbuf_get_height(icon_pixbuf)};
  89. if (icon)
  90. icon->pixbuf = std::move(icon_pixbuf);
  91. return size;
  92. }
  93. auto icon_paintable = Gtk4IconThemeLookupIcon(
  94. GetDefaultIconTheme(), icon_name, nullptr, kNavButtonIconSize, scale,
  95. GTK_TEXT_DIR_NONE, static_cast<GtkIconLookupFlags>(0));
  96. auto* paintable =
  97. GlibCast<GdkPaintable>(icon_paintable.get(), gdk_paintable_get_type());
  98. int width = scale * gdk_paintable_get_intrinsic_width(paintable);
  99. int height = scale * gdk_paintable_get_intrinsic_height(paintable);
  100. if (icon) {
  101. auto* snapshot = gtk_snapshot_new();
  102. gdk_paintable_snapshot(paintable, snapshot, width, height);
  103. auto* node = gtk_snapshot_free_to_node(snapshot);
  104. GdkTexture* texture = GetTextureFromRenderNode(node);
  105. size_t nbytes = width * height * sizeof(SkColor);
  106. SkColor* pixels = reinterpret_cast<SkColor*>(g_malloc(nbytes));
  107. size_t stride = sizeof(SkColor) * width;
  108. gdk_texture_download(texture, reinterpret_cast<guchar*>(pixels), stride);
  109. SkColor fg = GtkStyleContextGetColor(button_context);
  110. for (int i = 0; i < width * height; ++i)
  111. pixels[i] = SkColorSetA(fg, SkColorGetA(pixels[i]));
  112. icon->texture = TakeGObject(
  113. gdk_memory_texture_new(width, height, GDK_MEMORY_B8G8R8A8,
  114. g_bytes_new_take(pixels, nbytes), stride));
  115. gsk_render_node_unref(node);
  116. }
  117. return {width, height};
  118. }
  119. gfx::Size GetMinimumWidgetSize(gfx::Size content_size,
  120. GtkStyleContext* content_context,
  121. GtkCssContext widget_context) {
  122. gfx::Rect widget_rect = gfx::Rect(content_size);
  123. if (content_context)
  124. widget_rect.Inset(-GtkStyleContextGetMargin(content_context));
  125. int min_width = 0;
  126. int min_height = 0;
  127. // On GTK3, get the min size from the CSS directly.
  128. if (GtkCheckVersion(3, 20) && !GtkCheckVersion(4)) {
  129. GtkStyleContextGet(widget_context, "min-width", &min_width, "min-height",
  130. &min_height, nullptr);
  131. widget_rect.set_width(std::max(widget_rect.width(), min_width));
  132. widget_rect.set_height(std::max(widget_rect.height(), min_height));
  133. }
  134. widget_rect.Inset(-GtkStyleContextGetPadding(widget_context));
  135. widget_rect.Inset(-GtkStyleContextGetBorder(widget_context));
  136. // On GTK4, the CSS properties are hidden, so compute the min size indirectly,
  137. // which will include the border, margin, and padding. We can't take this
  138. // codepath on GTK3 since we only have a widget available in GTK4.
  139. if (GtkCheckVersion(4)) {
  140. gtk_widget_measure(widget_context.widget(), GTK_ORIENTATION_HORIZONTAL, -1,
  141. &min_width, nullptr, nullptr, nullptr);
  142. gtk_widget_measure(widget_context.widget(), GTK_ORIENTATION_VERTICAL, -1,
  143. &min_height, nullptr, nullptr, nullptr);
  144. // The returned "minimum size" is the drawn size of the widget, which
  145. // doesn't include the margin. However, GTK includes this size in its
  146. // calculation. So remove the margin, recompute the min size, then add it
  147. // back.
  148. auto margin = GtkStyleContextGetMargin(widget_context);
  149. widget_rect.Inset(-margin);
  150. widget_rect.set_width(std::max(widget_rect.width(), min_width));
  151. widget_rect.set_height(std::max(widget_rect.height(), min_height));
  152. widget_rect.Inset(margin);
  153. }
  154. return widget_rect.size();
  155. }
  156. GtkCssContext CreateHeaderContext(bool maximized) {
  157. std::string window_selector = "GtkWindow#window.background.csd";
  158. if (maximized)
  159. window_selector += ".maximized";
  160. return AppendCssNodeToStyleContext(
  161. AppendCssNodeToStyleContext({}, window_selector),
  162. "GtkHeaderBar#headerbar.header-bar.titlebar");
  163. }
  164. GtkCssContext CreateWindowControlsContext(bool maximized) {
  165. return AppendCssNodeToStyleContext(CreateHeaderContext(maximized),
  166. "#windowcontrols");
  167. }
  168. void CalculateUnscaledButtonSize(
  169. ui::NavButtonProvider::FrameButtonDisplayType type,
  170. bool maximized,
  171. gfx::Size* button_size,
  172. gfx::Insets* button_margin) {
  173. // views::ImageButton expects the images for each state to be of the
  174. // same size, but GTK can, in general, use a differnetly-sized
  175. // button for each state. For this reason, render buttons for all
  176. // states at the size of a GTK_STATE_FLAG_NORMAL button.
  177. auto button_context = AppendCssNodeToStyleContext(
  178. CreateWindowControlsContext(maximized),
  179. "GtkButton#button.titlebutton." +
  180. std::string(ButtonStyleClassFromButtonType(type)));
  181. auto icon_size = LoadNavButtonIcon(type, button_context, 1);
  182. auto image_context =
  183. AppendCssNodeToStyleContext(button_context, "GtkImage#image");
  184. gfx::Size image_size =
  185. GetMinimumWidgetSize(icon_size, nullptr, image_context);
  186. *button_size =
  187. GetMinimumWidgetSize(image_size, image_context, button_context);
  188. *button_margin = GtkStyleContextGetMargin(button_context);
  189. }
  190. class NavButtonImageSource : public gfx::ImageSkiaSource {
  191. public:
  192. NavButtonImageSource(ui::NavButtonProvider::FrameButtonDisplayType type,
  193. ui::NavButtonProvider::ButtonState state,
  194. bool maximized,
  195. bool active,
  196. gfx::Size button_size)
  197. : type_(type),
  198. state_(state),
  199. maximized_(maximized),
  200. active_(active),
  201. button_size_(button_size) {}
  202. ~NavButtonImageSource() override = default;
  203. gfx::ImageSkiaRep GetImageForScale(float scale) override {
  204. // gfx::ImageSkia kindly caches the result of this function, so
  205. // RenderNavButton() is called at most once for each needed scale
  206. // factor. Additionally, buttons in the HOVERED or PRESSED states
  207. // are not actually rendered until they are needed.
  208. if (button_size_.IsEmpty())
  209. return gfx::ImageSkiaRep();
  210. auto button_context =
  211. AppendCssNodeToStyleContext(CreateWindowControlsContext(maximized_),
  212. "GtkButton#button.titlebutton");
  213. gtk_style_context_add_class(button_context,
  214. ButtonStyleClassFromButtonType(type_));
  215. GtkStateFlags button_state = GtkStateFlagsFromButtonState(state_);
  216. if (!active_) {
  217. button_state =
  218. static_cast<GtkStateFlags>(button_state | GTK_STATE_FLAG_BACKDROP);
  219. }
  220. gtk_style_context_set_state(button_context, button_state);
  221. // Gtk header bars usually have the same height in both maximized and
  222. // restored windows. But chrome's tabstrip background has a smaller height
  223. // when maximized. To prevent buttons from clipping outside of this region,
  224. // they are scaled down. However, this is problematic for themes that do
  225. // not expect this case and use bitmaps for frame buttons (like the Breeze
  226. // theme). When the background-size is set to auto, the background bitmap
  227. // is not scaled for the (unexpected) smaller button size, and the button's
  228. // edges appear cut off. To fix this, manually set the background to scale
  229. // to the button size when it would have clipped.
  230. //
  231. // GTK's "contain" is unlike CSS's "contain". In CSS, the image would only
  232. // be downsized when it would have clipped. In GTK, the image is always
  233. // scaled to fit the drawing region (preserving aspect ratio). Only add
  234. // "contain" if clipping would occur.
  235. int bg_width = 0;
  236. int bg_height = 0;
  237. if (GtkCheckVersion(4)) {
  238. auto* snapshot = gtk_snapshot_new();
  239. gtk_snapshot_render_background(snapshot, button_context, 0, 0,
  240. button_size_.width(),
  241. button_size_.height());
  242. if (auto* node = gtk_snapshot_free_to_node(snapshot)) {
  243. if (GdkTexture* texture = GetTextureFromRenderNode(node)) {
  244. bg_width = gdk_texture_get_width(texture);
  245. bg_height = gdk_texture_get_height(texture);
  246. }
  247. gsk_render_node_unref(node);
  248. }
  249. } else {
  250. cairo_pattern_t* cr_pattern = nullptr;
  251. cairo_surface_t* cr_surface = nullptr;
  252. GtkStyleContextGet(
  253. button_context,
  254. "background-image" /* GTK_STYLE_PROPERTY_BACKGROUND_IMAGE */,
  255. &cr_pattern, nullptr);
  256. if (cr_pattern) {
  257. cairo_pattern_get_surface(cr_pattern, &cr_surface);
  258. if (cr_surface &&
  259. cairo_surface_get_type(cr_surface) == CAIRO_SURFACE_TYPE_IMAGE) {
  260. bg_width = cairo_image_surface_get_width(cr_surface);
  261. bg_height = cairo_image_surface_get_height(cr_surface);
  262. }
  263. cairo_pattern_destroy(cr_pattern);
  264. }
  265. }
  266. if (bg_width > button_size_.width() || bg_height > button_size_.height()) {
  267. ApplyCssToContext(button_context,
  268. ".titlebutton { background-size: contain; }");
  269. }
  270. // Gtk doesn't support fractional scale factors, but chrome does.
  271. // Rendering the button background and border at a fractional
  272. // scale factor is easy, since we can adjust the cairo context
  273. // transform. But the icon is loaded from a pixbuf, so we pick
  274. // the next-highest integer scale and manually downsize.
  275. int pixbuf_scale = scale == static_cast<int>(scale) ? scale : scale + 1;
  276. NavButtonIcon icon;
  277. auto icon_size =
  278. LoadNavButtonIcon(type_, button_context, pixbuf_scale, &icon);
  279. SkBitmap bitmap;
  280. bitmap.allocN32Pixels(scale * button_size_.width(),
  281. scale * button_size_.height());
  282. bitmap.eraseColor(0);
  283. CairoSurface surface(bitmap);
  284. cairo_t* cr = surface.cairo();
  285. cairo_save(cr);
  286. cairo_scale(cr, scale, scale);
  287. if (GtkCheckVersion(3, 11, 3) ||
  288. (button_state & (GTK_STATE_FLAG_PRELIGHT | GTK_STATE_FLAG_ACTIVE))) {
  289. gtk_render_background(button_context, cr, 0, 0, button_size_.width(),
  290. button_size_.height());
  291. gtk_render_frame(button_context, cr, 0, 0, button_size_.width(),
  292. button_size_.height());
  293. }
  294. cairo_restore(cr);
  295. cairo_save(cr);
  296. float pixbuf_extra_scale = scale / pixbuf_scale;
  297. cairo_scale(cr, pixbuf_extra_scale, pixbuf_extra_scale);
  298. GtkRenderIcon(
  299. button_context, cr, icon.pixbuf, icon.texture,
  300. ((pixbuf_scale * button_size_.width() - icon_size.width()) / 2),
  301. ((pixbuf_scale * button_size_.height() - icon_size.height()) / 2));
  302. cairo_restore(cr);
  303. return gfx::ImageSkiaRep(bitmap, scale);
  304. }
  305. bool HasRepresentationAtAllScales() const override { return true; }
  306. private:
  307. ui::NavButtonProvider::FrameButtonDisplayType type_;
  308. ui::NavButtonProvider::ButtonState state_;
  309. bool maximized_;
  310. bool active_;
  311. gfx::Size button_size_;
  312. };
  313. } // namespace
  314. NavButtonProviderGtk::NavButtonProviderGtk() = default;
  315. NavButtonProviderGtk::~NavButtonProviderGtk() = default;
  316. void NavButtonProviderGtk::RedrawImages(int top_area_height,
  317. bool maximized,
  318. bool active) {
  319. auto header_context = CreateHeaderContext(maximized);
  320. auto header_padding = GtkStyleContextGetPadding(header_context);
  321. double scale = 1.0f;
  322. std::map<ui::NavButtonProvider::FrameButtonDisplayType, gfx::Size>
  323. button_sizes;
  324. std::map<ui::NavButtonProvider::FrameButtonDisplayType, gfx::Insets>
  325. button_margins;
  326. std::vector<ui::NavButtonProvider::FrameButtonDisplayType> display_types{
  327. ui::NavButtonProvider::FrameButtonDisplayType::kMinimize,
  328. maximized ? ui::NavButtonProvider::FrameButtonDisplayType::kRestore
  329. : ui::NavButtonProvider::FrameButtonDisplayType::kMaximize,
  330. ui::NavButtonProvider::FrameButtonDisplayType::kClose,
  331. };
  332. for (auto type : display_types) {
  333. CalculateUnscaledButtonSize(type, maximized, &button_sizes[type],
  334. &button_margins[type]);
  335. int button_unconstrained_height = button_sizes[type].height() +
  336. button_margins[type].top() +
  337. button_margins[type].bottom();
  338. int needed_height = header_padding.top() + button_unconstrained_height +
  339. header_padding.bottom();
  340. if (needed_height > top_area_height)
  341. scale =
  342. std::min(scale, static_cast<double>(top_area_height) / needed_height);
  343. }
  344. top_area_spacing_ =
  345. gfx::Insets::TLBR(std::round(scale * header_padding.top()),
  346. std::round(scale * header_padding.left()),
  347. std::round(scale * header_padding.bottom()),
  348. std::round(scale * header_padding.right()));
  349. inter_button_spacing_ = std::round(scale * kHeaderSpacing);
  350. for (auto type : display_types) {
  351. double button_height =
  352. scale * (button_sizes[type].height() + button_margins[type].top() +
  353. button_margins[type].bottom());
  354. double available_height =
  355. top_area_height -
  356. scale * (header_padding.top() + header_padding.bottom());
  357. double scaled_button_offset = (available_height - button_height) / 2;
  358. gfx::Size size = button_sizes[type];
  359. size = gfx::Size(std::round(scale * size.width()),
  360. std::round(scale * size.height()));
  361. gfx::Insets margin = button_margins[type];
  362. margin = gfx::Insets::TLBR(
  363. std::round(scale * (header_padding.top() + margin.top()) +
  364. scaled_button_offset),
  365. std::round(scale * margin.left()), 0,
  366. std::round(scale * margin.right()));
  367. button_margins_[type] = margin;
  368. for (auto state : {
  369. ui::NavButtonProvider::ButtonState::kNormal,
  370. ui::NavButtonProvider::ButtonState::kHovered,
  371. ui::NavButtonProvider::ButtonState::kPressed,
  372. ui::NavButtonProvider::ButtonState::kDisabled,
  373. }) {
  374. button_images_[type][state] =
  375. gfx::ImageSkia(std::make_unique<NavButtonImageSource>(
  376. type, state, maximized, active, size),
  377. size);
  378. }
  379. }
  380. }
  381. gfx::ImageSkia NavButtonProviderGtk::GetImage(
  382. ui::NavButtonProvider::FrameButtonDisplayType type,
  383. ui::NavButtonProvider::ButtonState state) const {
  384. auto it = button_images_.find(type);
  385. DCHECK(it != button_images_.end());
  386. auto it2 = it->second.find(state);
  387. DCHECK(it2 != it->second.end());
  388. return it2->second;
  389. }
  390. gfx::Insets NavButtonProviderGtk::GetNavButtonMargin(
  391. ui::NavButtonProvider::FrameButtonDisplayType type) const {
  392. auto it = button_margins_.find(type);
  393. DCHECK(it != button_margins_.end());
  394. return it->second;
  395. }
  396. gfx::Insets NavButtonProviderGtk::GetTopAreaSpacing() const {
  397. return top_area_spacing_;
  398. }
  399. int NavButtonProviderGtk::GetInterNavButtonSpacing() const {
  400. return inter_button_spacing_;
  401. }
  402. } // namespace gtk