1// Copyright (c) 2011 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 "chrome/browser/ui/gtk/tabs/dragged_tab_gtk.h"
6
7#include <gdk/gdk.h>
8
9#include <algorithm>
10
11#include "base/i18n/rtl.h"
12#include "chrome/browser/extensions/extension_tab_helper.h"
13#include "chrome/browser/profiles/profile.h"
14#include "chrome/browser/tabs/tab_strip_model.h"
15#include "chrome/browser/themes/theme_service.h"
16#include "chrome/browser/themes/theme_service_factory.h"
17#include "chrome/browser/ui/gtk/gtk_util.h"
18#include "chrome/browser/ui/gtk/tabs/tab_renderer_gtk.h"
19#include "chrome/browser/ui/tab_contents/tab_contents_wrapper.h"
20#include "content/browser/renderer_host/backing_store_x.h"
21#include "content/browser/renderer_host/render_view_host.h"
22#include "content/browser/tab_contents/tab_contents.h"
23#include "third_party/skia/include/core/SkShader.h"
24#include "ui/base/x/x11_util.h"
25#include "ui/gfx/gtk_util.h"
26
27namespace {
28
29// The size of the dragged window frame.
30const int kDragFrameBorderSize = 1;
31const int kTwiceDragFrameBorderSize = 2 * kDragFrameBorderSize;
32
33// Used to scale the dragged window sizes.
34const float kScalingFactor = 0.5;
35
36const int kAnimateToBoundsDurationMs = 150;
37
38const gdouble kTransparentAlpha = (200.0f / 255.0f);
39const gdouble kOpaqueAlpha = 1.0f;
40const double kDraggedTabBorderColor[] = { 103.0 / 0xff,
41                                          129.0 / 0xff,
42                                          162.0 / 0xff };
43
44}  // namespace
45
46////////////////////////////////////////////////////////////////////////////////
47// DraggedTabGtk, public:
48
49DraggedTabGtk::DraggedTabGtk(TabContents* datasource,
50                             const gfx::Point& mouse_tab_offset,
51                             const gfx::Size& contents_size,
52                             bool mini)
53    : data_source_(datasource),
54      renderer_(new TabRendererGtk(ThemeServiceFactory::GetForProfile(
55          datasource->profile()))),
56      attached_(false),
57      mouse_tab_offset_(mouse_tab_offset),
58      attached_tab_size_(TabRendererGtk::GetMinimumSelectedSize()),
59      contents_size_(contents_size),
60      close_animation_(this) {
61  TabContentsWrapper* wrapper =
62      TabContentsWrapper::GetCurrentWrapperForContents(datasource);
63  renderer_->UpdateData(datasource,
64                        wrapper->extension_tab_helper()->is_app(),
65                        false); // loading_only
66  renderer_->set_mini(mini);
67
68  container_ = gtk_window_new(GTK_WINDOW_POPUP);
69  SetContainerColorMap();
70  gtk_widget_set_app_paintable(container_, TRUE);
71  g_signal_connect(container_, "expose-event",
72                   G_CALLBACK(OnExposeEvent), this);
73  gtk_widget_add_events(container_, GDK_STRUCTURE_MASK);
74
75  // We contain the tab renderer in a GtkFixed in order to maintain the
76  // requested size.  Otherwise, the widget will fill the entire window and
77  // cause a crash when rendering because the bounds don't match our images.
78  fixed_ = gtk_fixed_new();
79  gtk_fixed_put(GTK_FIXED(fixed_), renderer_->widget(), 0, 0);
80  gtk_container_add(GTK_CONTAINER(container_), fixed_);
81  gtk_widget_show_all(container_);
82}
83
84DraggedTabGtk::~DraggedTabGtk() {
85  gtk_widget_destroy(container_);
86}
87
88void DraggedTabGtk::MoveTo(const gfx::Point& screen_point) {
89  int x = screen_point.x() + mouse_tab_offset_.x() -
90      ScaleValue(mouse_tab_offset_.x());
91  int y = screen_point.y() + mouse_tab_offset_.y() -
92      ScaleValue(mouse_tab_offset_.y());
93
94  gtk_window_move(GTK_WINDOW(container_), x, y);
95}
96
97void DraggedTabGtk::Attach(int selected_width) {
98  attached_ = true;
99  Resize(selected_width);
100
101  if (gtk_util::IsScreenComposited())
102    gdk_window_set_opacity(container_->window, kOpaqueAlpha);
103}
104
105void DraggedTabGtk::Resize(int width) {
106  attached_tab_size_.set_width(width);
107  ResizeContainer();
108}
109
110void DraggedTabGtk::Detach() {
111  attached_ = false;
112  ResizeContainer();
113
114  if (gtk_util::IsScreenComposited())
115    gdk_window_set_opacity(container_->window, kTransparentAlpha);
116}
117
118void DraggedTabGtk::Update() {
119  gtk_widget_queue_draw(container_);
120}
121
122void DraggedTabGtk::AnimateToBounds(const gfx::Rect& bounds,
123                                    AnimateToBoundsCallback* callback) {
124  animation_callback_.reset(callback);
125
126  gint x, y, width, height;
127  gdk_window_get_origin(container_->window, &x, &y);
128  gdk_window_get_geometry(container_->window, NULL, NULL,
129                          &width, &height, NULL);
130
131  animation_start_bounds_ = gfx::Rect(x, y, width, height);
132  animation_end_bounds_ = bounds;
133
134  close_animation_.SetSlideDuration(kAnimateToBoundsDurationMs);
135  close_animation_.SetTweenType(ui::Tween::EASE_OUT);
136  if (!close_animation_.IsShowing()) {
137    close_animation_.Reset();
138    close_animation_.Show();
139  }
140}
141
142////////////////////////////////////////////////////////////////////////////////
143// DraggedTabGtk, ui::AnimationDelegate implementation:
144
145void DraggedTabGtk::AnimationProgressed(const ui::Animation* animation) {
146  int delta_x = (animation_end_bounds_.x() - animation_start_bounds_.x());
147  int x = animation_start_bounds_.x() +
148      static_cast<int>(delta_x * animation->GetCurrentValue());
149  int y = animation_end_bounds_.y();
150  gdk_window_move(container_->window, x, y);
151}
152
153void DraggedTabGtk::AnimationEnded(const ui::Animation* animation) {
154  animation_callback_->Run();
155}
156
157void DraggedTabGtk::AnimationCanceled(const ui::Animation* animation) {
158  AnimationEnded(animation);
159}
160
161////////////////////////////////////////////////////////////////////////////////
162// DraggedTabGtk, private:
163
164void DraggedTabGtk::Layout() {
165  if (attached_) {
166    renderer_->SetBounds(gfx::Rect(GetPreferredSize()));
167  } else {
168    int left = 0;
169    if (base::i18n::IsRTL())
170      left = GetPreferredSize().width() - attached_tab_size_.width();
171
172    // The renderer_'s width should be attached_tab_size_.width() in both LTR
173    // and RTL locales. Wrong width will cause the wrong positioning of the tab
174    // view in dragging. Please refer to http://crbug.com/6223 for details.
175    renderer_->SetBounds(gfx::Rect(left, 0, attached_tab_size_.width(),
176                         attached_tab_size_.height()));
177  }
178}
179
180gfx::Size DraggedTabGtk::GetPreferredSize() {
181  if (attached_)
182    return attached_tab_size_;
183
184  int width = std::max(attached_tab_size_.width(), contents_size_.width()) +
185      kTwiceDragFrameBorderSize;
186  int height = attached_tab_size_.height() + kDragFrameBorderSize +
187      contents_size_.height();
188  return gfx::Size(width, height);
189}
190
191void DraggedTabGtk::ResizeContainer() {
192  gfx::Size size = GetPreferredSize();
193  gtk_window_resize(GTK_WINDOW(container_),
194                    ScaleValue(size.width()), ScaleValue(size.height()));
195  Layout();
196}
197
198int DraggedTabGtk::ScaleValue(int value) {
199  return attached_ ? value : static_cast<int>(value * kScalingFactor);
200}
201
202gfx::Rect DraggedTabGtk::bounds() const {
203  gint x, y, width, height;
204  gtk_window_get_position(GTK_WINDOW(container_), &x, &y);
205  gtk_window_get_size(GTK_WINDOW(container_), &width, &height);
206  return gfx::Rect(x, y, width, height);
207}
208
209void DraggedTabGtk::SetContainerColorMap() {
210  GdkScreen* screen = gtk_widget_get_screen(container_);
211  GdkColormap* colormap = gdk_screen_get_rgba_colormap(screen);
212
213  // If rgba is not available, use rgb instead.
214  if (!colormap)
215    colormap = gdk_screen_get_rgb_colormap(screen);
216
217  gtk_widget_set_colormap(container_, colormap);
218}
219
220void DraggedTabGtk::SetContainerTransparency() {
221  cairo_t* cairo_context = gdk_cairo_create(container_->window);
222  if (!cairo_context)
223    return;
224
225  // Make the background of the dragged tab window fully transparent.  All of
226  // the content of the window (child widgets) will be completely opaque.
227  gfx::Size size = bounds().size();
228  cairo_scale(cairo_context, static_cast<double>(size.width()),
229              static_cast<double>(size.height()));
230  cairo_set_source_rgba(cairo_context, 1.0f, 1.0f, 1.0f, 0.0f);
231  cairo_set_operator(cairo_context, CAIRO_OPERATOR_SOURCE);
232  cairo_paint(cairo_context);
233  cairo_destroy(cairo_context);
234}
235
236void DraggedTabGtk::SetContainerShapeMask(cairo_surface_t* surface) {
237  // Create a 1bpp bitmap the size of |container_|.
238  gfx::Size size = bounds().size();
239  GdkPixmap* pixmap = gdk_pixmap_new(NULL, size.width(), size.height(), 1);
240  cairo_t* cairo_context = gdk_cairo_create(GDK_DRAWABLE(pixmap));
241
242  // Set the transparency.
243  cairo_set_source_rgba(cairo_context, 1.0f, 1.0f, 1.0f, 0.0f);
244
245  // Blit the rendered bitmap into a pixmap.  Any pixel set in the pixmap will
246  // be opaque in the container window.
247  cairo_set_operator(cairo_context, CAIRO_OPERATOR_SOURCE);
248  if (!attached_)
249    cairo_scale(cairo_context, kScalingFactor, kScalingFactor);
250  cairo_set_source_surface(cairo_context, surface, 0, 0);
251  cairo_paint(cairo_context);
252
253  if (!attached_) {
254    // Make the render area depiction opaque (leaving enough room for the
255    // border).
256    cairo_identity_matrix(cairo_context);
257    // On Lucid running VNC, the X server will reject RGBA (1,1,1,1) as an
258    // invalid value below in gdk_window_shape_combine_mask(). Using (0,0,0,1)
259    // instead. The value doesn't really matter, as long as the alpha is not 0.
260    cairo_set_source_rgba(cairo_context, 0.0f, 0.0f, 0.0f, 1.0f);
261    int tab_height = static_cast<int>(kScalingFactor *
262                                      renderer_->height() -
263                                      kDragFrameBorderSize);
264    cairo_rectangle(cairo_context,
265                    0, tab_height,
266                    size.width(), size.height() - tab_height);
267    cairo_fill(cairo_context);
268  }
269
270  cairo_destroy(cairo_context);
271
272  // Set the shape mask.
273  gdk_window_shape_combine_mask(container_->window, pixmap, 0, 0);
274  g_object_unref(pixmap);
275}
276
277// static
278gboolean DraggedTabGtk::OnExposeEvent(GtkWidget* widget,
279                                      GdkEventExpose* event,
280                                      DraggedTabGtk* dragged_tab) {
281  cairo_surface_t* surface = dragged_tab->renderer_->PaintToSurface();
282  if (gtk_util::IsScreenComposited()) {
283    dragged_tab->SetContainerTransparency();
284  } else {
285    dragged_tab->SetContainerShapeMask(surface);
286  }
287
288  // Only used when not attached.
289  int tab_width = static_cast<int>(kScalingFactor *
290      dragged_tab->renderer_->width());
291  int tab_height = static_cast<int>(kScalingFactor *
292      dragged_tab->renderer_->height());
293
294  // Draw the render area.
295  BackingStore* backing_store =
296      dragged_tab->data_source_->render_view_host()->GetBackingStore(false);
297  if (backing_store && !dragged_tab->attached_) {
298    // This leaves room for the border.
299    static_cast<BackingStoreX*>(backing_store)->PaintToRect(
300        gfx::Rect(kDragFrameBorderSize, tab_height,
301                  widget->allocation.width - kTwiceDragFrameBorderSize,
302                  widget->allocation.height - tab_height -
303                  kDragFrameBorderSize),
304        GDK_DRAWABLE(widget->window));
305  }
306
307  cairo_t* cr = gdk_cairo_create(GDK_DRAWABLE(widget->window));
308  // Draw the border.
309  if (!dragged_tab->attached_) {
310    cairo_set_line_width(cr, kDragFrameBorderSize);
311    cairo_set_source_rgb(cr, kDraggedTabBorderColor[0],
312                             kDraggedTabBorderColor[1],
313                             kDraggedTabBorderColor[2]);
314    // |offset| is the distance from the edge of the image to the middle of
315    // the border line.
316    double offset = kDragFrameBorderSize / 2.0 - 0.5;
317    double left_x = offset;
318    double top_y = tab_height - kDragFrameBorderSize + offset;
319    double right_x = widget->allocation.width - offset;
320    double bottom_y = widget->allocation.height - offset;
321    double middle_x = tab_width + offset;
322
323    // We don't use cairo_rectangle() because we don't want to draw the border
324    // under the tab itself.
325    cairo_move_to(cr, left_x, top_y);
326    cairo_line_to(cr, left_x, bottom_y);
327    cairo_line_to(cr, right_x, bottom_y);
328    cairo_line_to(cr, right_x, top_y);
329    cairo_line_to(cr, middle_x, top_y);
330    cairo_stroke(cr);
331  }
332
333  // Draw the tab.
334  if (!dragged_tab->attached_)
335    cairo_scale(cr, kScalingFactor, kScalingFactor);
336  cairo_set_source_surface(cr, surface, 0, 0);
337  cairo_paint(cr);
338
339  cairo_destroy(cr);
340
341  cairo_surface_destroy(surface);
342
343  // We've already drawn the tab, so don't propagate the expose-event signal.
344  return TRUE;
345}
346