page-widget.c 42.1 KB
Newer Older
1
/* SPDX-License-Identifier: Zlib */
2 3 4

#include <girara/utils.h>
#include <girara/settings.h>
5
#include <girara/datastructures.h>
6 7 8
#include <girara/session.h>
#include <string.h>
#include <glib/gi18n.h>
9
#include <math.h>
10

Moritz Lipp's avatar
Moritz Lipp committed
11
#include "links.h"
12
#include "page-widget.h"
Moritz Lipp's avatar
Moritz Lipp committed
13
#include "page.h"
14 15 16
#include "render.h"
#include "utils.h"
#include "shortcuts.h"
17
#include "zathura.h"
18

19
typedef struct zathura_page_widget_private_s {
20 21
  zathura_page_t* page; /**< Page object */
  zathura_t* zathura; /**< Zathura object */
22
  cairo_surface_t* surface; /**< Cairo surface */
23
  cairo_surface_t* thumbnail; /**< Cairo surface */
24
  ZathuraRenderRequest* render_request; /* Request object */
25
  bool cached; /**< Cached state */
26 27 28

  struct {
    girara_list_t* list; /**< List of links on the page */
29 30
    gboolean retrieved; /**< True if we already tried to retrieve the list of links */
    gboolean draw; /**< True if links should be drawn */
31 32 33 34 35 36 37
    unsigned int offset; /**< Offset to the links */
    unsigned int n; /**< Number */
  } links;

  struct {
    girara_list_t* list; /**< A list if there are search results that should be drawn */
    int current; /**< The index of the current search result */
38
    gboolean draw; /**< Draw search results */
39 40 41 42
  } search;

  struct {
    girara_list_t* list; /**< List of images on the page */
43
    gboolean retrieved; /**< True if we already tried to retrieve the list of images */
44 45 46
    zathura_image_t* current; /**< Image data of selected image */
  } images;

47
  struct {
48 49 50 51 52
    zathura_rectangle_t selection; /**< Region selected with the mouse */
    struct {
      int x; /**< X coordinate */
      int y; /**< Y coordinate */
    } selection_basepoint;
53
    gboolean over_link;
54
  } mouse;
Sebastian Ramacher's avatar
Sebastian Ramacher committed
55
} ZathuraPagePrivate;
56

Sebastian Ramacher's avatar
Sebastian Ramacher committed
57
G_DEFINE_TYPE_WITH_CODE(ZathuraPage, zathura_page_widget, GTK_TYPE_DRAWING_AREA, G_ADD_PRIVATE(ZathuraPage))
58

59
static gboolean zathura_page_widget_draw(GtkWidget* widget, cairo_t* cairo);
60
static void zathura_page_widget_finalize(GObject* object);
61
static void zathura_page_widget_dispose(GObject* object);
62 63 64 65 66
static void zathura_page_widget_set_property(GObject* object, guint prop_id, const GValue* value, GParamSpec* pspec);
static void zathura_page_widget_get_property(GObject* object, guint prop_id, GValue* value, GParamSpec* pspec);
static void zathura_page_widget_size_allocate(GtkWidget* widget, GdkRectangle* allocation);
static void redraw_rect(ZathuraPage* widget, zathura_rectangle_t* rectangle);
static void redraw_all_rects(ZathuraPage* widget, girara_list_t* rectangles);
Moritz Lipp's avatar
Moritz Lipp committed
67
static void zathura_page_widget_popup_menu(GtkWidget* widget, GdkEventButton* event);
68
static gboolean cb_zathura_page_widget_button_press_event(GtkWidget* widget, GdkEventButton* button);
69 70
static gboolean cb_zathura_page_widget_button_release_event(GtkWidget* widget, GdkEventButton* button);
static gboolean cb_zathura_page_widget_motion_notify(GtkWidget* widget, GdkEventMotion* event);
71
static gboolean cb_zathura_page_widget_leave_notify(GtkWidget* widget, GdkEventCrossing* event);
Moritz Lipp's avatar
Moritz Lipp committed
72 73
static gboolean cb_zathura_page_widget_popup_menu(GtkWidget* widget);
static void cb_menu_image_copy(GtkMenuItem* item, ZathuraPage* page);
74
static void cb_menu_image_save(GtkMenuItem* item, ZathuraPage* page);
75
static void cb_update_surface(ZathuraRenderRequest* request, cairo_surface_t* surface, void* data);
76 77
static void cb_cache_added(ZathuraRenderRequest* request, void* data);
static void cb_cache_invalidated(ZathuraRenderRequest* request, void* data);
78 79
static bool surface_small_enough(cairo_surface_t* surface, size_t max_size, cairo_surface_t* old);
static cairo_surface_t *draw_thumbnail_image(cairo_surface_t* surface, size_t max_size);
80

Moritz Lipp's avatar
Moritz Lipp committed
81
enum properties_e {
82
  PROP_0,
83
  PROP_PAGE,
84
  PROP_ZATHURA,
85
  PROP_DRAW_LINKS,
86 87
  PROP_LINKS_OFFSET,
  PROP_LINKS_NUMBER,
88 89
  PROP_SEARCH_RESULTS,
  PROP_SEARCH_RESULTS_LENGTH,
90
  PROP_SEARCH_RESULTS_CURRENT,
91
  PROP_DRAW_SEARCH_RESULTS,
92
  PROP_LAST_VIEW,
93 94
};

95 96 97
enum {
  TEXT_SELECTED,
  IMAGE_SELECTED,
98
  BUTTON_RELEASE,
99 100
  ENTER_LINK,
  LEAVE_LINK,
101 102 103 104 105
  LAST_SIGNAL
};

static guint signals[LAST_SIGNAL] = { 0 };

106
static void
107
zathura_page_widget_class_init(ZathuraPageClass* class)
108 109
{
  /* overwrite methods */
Sebastian Ramacher's avatar
Sebastian Ramacher committed
110
  GtkWidgetClass* widget_class       = GTK_WIDGET_CLASS(class);
111
  widget_class->draw                 = zathura_page_widget_draw;
112 113
  widget_class->size_allocate        = zathura_page_widget_size_allocate;
  widget_class->button_press_event   = cb_zathura_page_widget_button_press_event;
114
  widget_class->button_release_event = cb_zathura_page_widget_button_release_event;
115
  widget_class->motion_notify_event  = cb_zathura_page_widget_motion_notify;
116
  widget_class->leave_notify_event   = cb_zathura_page_widget_leave_notify;
Moritz Lipp's avatar
Moritz Lipp committed
117
  widget_class->popup_menu           = cb_zathura_page_widget_popup_menu;
118 119

  GObjectClass* object_class = G_OBJECT_CLASS(class);
120
  object_class->dispose      = zathura_page_widget_dispose;
121
  object_class->finalize     = zathura_page_widget_finalize;
122 123
  object_class->set_property = zathura_page_widget_set_property;
  object_class->get_property = zathura_page_widget_get_property;
124 125 126

  /* add properties */
  g_object_class_install_property(object_class, PROP_PAGE,
127
                                  g_param_spec_pointer("page", "page", "the page to draw", G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
128
  g_object_class_install_property(object_class, PROP_ZATHURA,
129
                                  g_param_spec_pointer("zathura", "zathura", "the zathura instance", G_PARAM_WRITABLE | G_PARAM_CONSTRUCT_ONLY | G_PARAM_STATIC_STRINGS));
130
  g_object_class_install_property(object_class, PROP_DRAW_LINKS,
131
                                  g_param_spec_boolean("draw-links", "draw-links", "Set to true if links should be drawn", FALSE, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
132
  g_object_class_install_property(object_class, PROP_LINKS_OFFSET,
133
                                  g_param_spec_int("offset-links", "offset-links", "Offset for the link numbers", 0, INT_MAX, 0, G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
134
  g_object_class_install_property(object_class, PROP_LINKS_NUMBER,
135
                                  g_param_spec_int("number-of-links", "number-of-links", "Number of links", 0, INT_MAX, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
136
  g_object_class_install_property(object_class, PROP_SEARCH_RESULTS,
137
                                  g_param_spec_pointer("search-results", "search-results", "Set to the list of search results", G_PARAM_WRITABLE | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
138
  g_object_class_install_property(object_class, PROP_SEARCH_RESULTS_CURRENT,
139
                                  g_param_spec_int("search-current", "search-current", "The current search result", -1, INT_MAX, 0, G_PARAM_WRITABLE | G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
140
  g_object_class_install_property(object_class, PROP_SEARCH_RESULTS_LENGTH,
141
                                  g_param_spec_int("search-length", "search-length", "The number of search results", -1, INT_MAX, 0, G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
142 143
  g_object_class_install_property(object_class, PROP_DRAW_SEARCH_RESULTS,
                                  g_param_spec_boolean("draw-search-results", "draw-search-results", "Set to true if search results should be drawn", FALSE, G_PARAM_READABLE | G_PARAM_WRITABLE | G_PARAM_STATIC_STRINGS));
144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166

  /* add signals */
  signals[TEXT_SELECTED] = g_signal_new("text-selected",
      ZATHURA_TYPE_PAGE,
      G_SIGNAL_RUN_LAST,
      0,
      NULL,
      NULL,
      g_cclosure_marshal_generic,
      G_TYPE_NONE,
      1,
      G_TYPE_STRING);

  signals[IMAGE_SELECTED] = g_signal_new("image-selected",
      ZATHURA_TYPE_PAGE,
      G_SIGNAL_RUN_LAST,
      0,
      NULL,
      NULL,
      g_cclosure_marshal_generic,
      G_TYPE_NONE,
      1,
      G_TYPE_OBJECT);
167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186

  signals[ENTER_LINK] = g_signal_new("enter-link",
      ZATHURA_TYPE_PAGE,
      G_SIGNAL_RUN_LAST,
      0,
      NULL,
      NULL,
      g_cclosure_marshal_generic,
      G_TYPE_NONE,
      0);

  signals[LEAVE_LINK] = g_signal_new("leave-link",
      ZATHURA_TYPE_PAGE,
      G_SIGNAL_RUN_LAST,
      0,
      NULL,
      NULL,
      g_cclosure_marshal_generic,
      G_TYPE_NONE,
      0);
187 188 189 190 191 192 193 194 195 196 197

  signals[BUTTON_RELEASE] = g_signal_new("scaled-button-release",
      ZATHURA_TYPE_PAGE,
      G_SIGNAL_RUN_LAST,
      0,
      NULL,
      NULL,
      g_cclosure_marshal_generic,
      G_TYPE_NONE,
      1,
      G_TYPE_POINTER);
198 199 200
}

static void
201
zathura_page_widget_init(ZathuraPage* widget)
202
{
Sebastian Ramacher's avatar
Sebastian Ramacher committed
203 204 205 206 207 208
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
  priv->page               = NULL;
  priv->surface            = NULL;
  priv->thumbnail          = NULL;
  priv->render_request     = NULL;
  priv->cached             = false;
209 210 211 212 213 214 215 216 217

  priv->links.list      = NULL;
  priv->links.retrieved = false;
  priv->links.draw      = false;
  priv->links.offset    = 0;
  priv->links.n         = 0;

  priv->search.list    = NULL;
  priv->search.current = INT_MAX;
218
  priv->search.draw    = false;
219 220 221 222 223 224 225 226 227 228

  priv->images.list      = NULL;
  priv->images.retrieved = false;
  priv->images.current   = NULL;

  priv->mouse.selection.x1          = -1;
  priv->mouse.selection.y1          = -1;
  priv->mouse.selection_basepoint.x = -1;
  priv->mouse.selection_basepoint.y = -1;

229 230 231
  const unsigned int event_mask = GDK_BUTTON_PRESS_MASK |
    GDK_BUTTON_RELEASE_MASK | GDK_POINTER_MOTION_MASK | GDK_LEAVE_NOTIFY_MASK;
  gtk_widget_add_events(GTK_WIDGET(widget), event_mask);
232 233 234
}

GtkWidget*
235
zathura_page_widget_new(zathura_t* zathura, zathura_page_t* page)
236 237 238
{
  g_return_val_if_fail(page != NULL, NULL);

239 240 241 242 243 244
  GObject* ret = g_object_new(ZATHURA_TYPE_PAGE, "page", page, "zathura", zathura, NULL);
  if (ret == NULL) {
    return NULL;
  }

  ZathuraPage* widget = ZATHURA_PAGE(ret);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
245
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
246 247
  priv->render_request = zathura_render_request_new(zathura->sync.render_thread, page);
  g_signal_connect_object(priv->render_request, "completed",
248
      G_CALLBACK(cb_update_surface), widget, 0);
249 250 251 252
  g_signal_connect_object(priv->render_request, "cache-added",
      G_CALLBACK(cb_cache_added), widget, 0);
  g_signal_connect_object(priv->render_request, "cache-invalidated",
      G_CALLBACK(cb_cache_invalidated), widget, 0);
253 254

  return GTK_WIDGET(ret);
255 256
}

257 258 259 260
static void
zathura_page_widget_dispose(GObject* object)
{
  ZathuraPage* widget = ZATHURA_PAGE(object);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
261
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
262 263 264 265 266 267

  g_clear_object(&priv->render_request);

  G_OBJECT_CLASS(zathura_page_widget_parent_class)->dispose(object);
}

268
static void
269
zathura_page_widget_finalize(GObject* object)
270
{
271
  ZathuraPage* widget = ZATHURA_PAGE(object);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
272
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
273

Sebastian Ramacher's avatar
Sebastian Ramacher committed
274
  if (priv->surface != NULL) {
275 276
    cairo_surface_destroy(priv->surface);
  }
277

278 279 280 281
  if (priv->thumbnail != NULL) {
    cairo_surface_destroy(priv->thumbnail);
  }

282 283
  if (priv->search.list != NULL) {
    girara_list_free(priv->search.list);
284 285
  }

286 287
  if (priv->links.list != NULL) {
    girara_list_free(priv->links.list);
288 289
  }

290
  G_OBJECT_CLASS(zathura_page_widget_parent_class)->finalize(object);
291 292
}

293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312
static void
set_font_from_property(cairo_t* cairo, zathura_t* zathura, cairo_font_weight_t weight)
{
  if (zathura == NULL) {
    return;
  }

  /* get user font description */
  char* font = NULL;
  girara_setting_get(zathura->ui.session, "font", &font);
  if (font == NULL) {
    return;
  }

  /* use pango to extract font family and size */
  PangoFontDescription* descr = pango_font_description_from_string(font);

  const char* family = pango_font_description_get_family(descr);

  /* get font size: can be points or absolute.
Jeremie Knuesel's avatar
Jeremie Knuesel committed
313 314
   * absolute units: example: value 10*PANGO_SCALE = 10 (unscaled) device units (logical pixels)
   * point units:    example: value 10*PANGO_SCALE = 10 points = 10*(font dpi config / 72) device units */
315
  double size = pango_font_description_get_size(descr) / (double)PANGO_SCALE;
316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335

  /* convert point size to device units */
  if (!pango_font_description_get_size_is_absolute(descr)) {
    double font_dpi = 96.0;
    if (zathura->ui.session != NULL) {
      if (gtk_widget_has_screen(zathura->ui.session->gtk.view)) {
        GdkScreen* screen = gtk_widget_get_screen(zathura->ui.session->gtk.view);
        font_dpi = gdk_screen_get_resolution(screen);
      }
    }
    size = size * font_dpi / 72;
  }

  cairo_select_font_face(cairo, family, CAIRO_FONT_SLANT_NORMAL, weight);
  cairo_set_font_size(cairo, size);

  pango_font_description_free(descr);
  g_free(font);
}

336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368
static cairo_text_extents_t
get_text_extents(const char* string, zathura_t* zathura, cairo_font_weight_t weight) {
  cairo_text_extents_t text = {0,};

  if (zathura == NULL) {
    return text;
  }

  /* make dummy surface to satisfy API requirements */
  cairo_surface_t* surface = cairo_image_surface_create(CAIRO_FORMAT_RGB24, 0, 0);
  if (cairo_surface_status(surface) != CAIRO_STATUS_SUCCESS) {
    return text;
  }

  cairo_t* cairo = cairo_create(surface);
  if (cairo_status(cairo) != CAIRO_STATUS_SUCCESS) {
    cairo_surface_destroy(surface);
    return text;
  }

  set_font_from_property(cairo, zathura, weight);
  cairo_text_extents(cairo, string, &text);

  /* add some margin (for some reason the reported extents can be a bit short) */
  text.width += 6;
  text.height += 2;

  cairo_destroy(cairo);
  cairo_surface_destroy(surface);

  return text;
}

369
static void
370
zathura_page_widget_set_property(GObject* object, guint prop_id, const GValue* value, GParamSpec* pspec)
371
{
372
  ZathuraPage* pageview = ZATHURA_PAGE(object);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
373
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(pageview);
374

375 376
  cairo_text_extents_t text;

377 378
  switch (prop_id) {
    case PROP_PAGE:
379 380 381 382
      priv->page = g_value_get_pointer(value);
      break;
    case PROP_ZATHURA:
      priv->zathura = g_value_get_pointer(value);
383
      break;
384
    case PROP_DRAW_LINKS:
385
      priv->links.draw = g_value_get_boolean(value);
386
      /* get links */
387
      if (priv->links.draw == TRUE && priv->links.retrieved == FALSE) {
388
        priv->links.list      = zathura_page_links_get(priv->page, NULL);
389
        priv->links.retrieved = TRUE;
390
        priv->links.n         = (priv->links.list == NULL) ? 0 : girara_list_size(priv->links.list);
391 392
      }

393
      if (priv->links.retrieved == TRUE && priv->links.list != NULL) {
394 395 396
        /* get size of text that should be large enough for every link hint */
        text = get_text_extents("888", priv->zathura, CAIRO_FONT_WEIGHT_BOLD);

397 398 399 400 401 402 403 404 405 406 407 408
        GIRARA_LIST_FOREACH_BODY(priv->links.list, zathura_link_t*, link,
          if (link != NULL) {
            /* redraw link area */
            zathura_rectangle_t rectangle = recalc_rectangle(priv->page, zathura_link_get_position(link));
            redraw_rect(pageview, &rectangle);

            /* also redraw area for link hint */
            rectangle.x2 = rectangle.x1 + text.width;
            rectangle.y1 = rectangle.y2 - text.height;
            redraw_rect(pageview, &rectangle);
          }
        );
409 410
      }
      break;
411
    case PROP_LINKS_OFFSET:
412
      priv->links.offset = g_value_get_int(value);
413
      break;
414
    case PROP_SEARCH_RESULTS:
415 416 417
      if (priv->search.list != NULL && priv->search.draw) {
        redraw_all_rects(pageview, priv->search.list);
        girara_list_free(priv->search.list);
418
      }
419 420
      priv->search.list = g_value_get_pointer(value);
      if (priv->search.list != NULL && priv->search.draw) {
421
        priv->links.draw = FALSE;
422
        redraw_all_rects(pageview, priv->search.list);
423
      }
424
      priv->search.current = -1;
Sebastian Ramacher's avatar
Sebastian Ramacher committed
425
      break;
426
    case PROP_SEARCH_RESULTS_CURRENT: {
427 428 429
      g_return_if_fail(priv->search.list != NULL);
      if (priv->search.current >= 0 && priv->search.current < (signed) girara_list_size(priv->search.list)) {
        zathura_rectangle_t* rect = girara_list_nth(priv->search.list, priv->search.current);
430 431 432 433
        zathura_rectangle_t rectangle = recalc_rectangle(priv->page, *rect);
        redraw_rect(pageview, &rectangle);
      }
      int val = g_value_get_int(value);
434
      if (val < 0) {
435
        priv->search.current = girara_list_size(priv->search.list);
436
      } else {
437
        priv->search.current = val;
438
        if (priv->search.draw == TRUE && val >= 0 && val < (signed) girara_list_size(priv->search.list)) {
439 440
          zathura_rectangle_t* rect = girara_list_nth(priv->search.list, priv->search.current);
          zathura_rectangle_t rectangle = recalc_rectangle(priv->page, *rect);
441 442
          redraw_rect(pageview, &rectangle);
        }
443 444 445
      }
      break;
    }
446
    case PROP_DRAW_SEARCH_RESULTS:
447
      priv->search.draw = g_value_get_boolean(value);
448 449 450 451 452 453 454 455 456

      /*
       * we do the following instead of only redrawing the rectangles of the
       * search results in order to avoid the rectangular margins that appear
       * around the search terms after their highlighted rectangular areas are
       * redrawn without highlighting.
       */

      if (priv->search.list != NULL && zathura_page_get_visibility(priv->page)) {
457
        gtk_widget_queue_draw(GTK_WIDGET(object));
458
      }
459
      break;
460 461 462 463 464 465
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
  }
}

static void
466
zathura_page_widget_get_property(GObject* object, guint prop_id, GValue* value, GParamSpec* pspec)
467
{
468
  ZathuraPage* pageview = ZATHURA_PAGE(object);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
469
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(pageview);
470 471 472

  switch (prop_id) {
    case PROP_LINKS_NUMBER:
473
      g_value_set_int(value, priv->links.n);
474
      break;
475
    case PROP_SEARCH_RESULTS_LENGTH:
476
      g_value_set_int(value, priv->search.list == NULL ? 0 : girara_list_size(priv->search.list));
477 478
      break;
    case PROP_SEARCH_RESULTS_CURRENT:
479
      g_value_set_int(value, priv->search.list == NULL ? -1 : priv->search.current);
480
      break;
481
    case PROP_SEARCH_RESULTS:
482
      g_value_set_pointer(value, priv->search.list);
483
      break;
484 485 486
    case PROP_DRAW_SEARCH_RESULTS:
      g_value_set_boolean(value, priv->search.draw);
      break;
487 488 489 490 491
    default:
      G_OBJECT_WARN_INVALID_PROPERTY_ID(object, prop_id, pspec);
  }
}

492 493 494
static zathura_device_factors_t
get_safe_device_factors(cairo_surface_t* surface)
{
495 496
  zathura_device_factors_t factors;
  cairo_surface_get_device_scale(surface, &factors.x, &factors.y);
497

498 499 500 501 502 503
  if (fabs(factors.x) < DBL_EPSILON) {
    factors.x = 1.0;
  }
  if (fabs(factors.y) < DBL_EPSILON) {
    factors.y = 1.0;
  }
504

505
  return factors;
506 507
}

508 509 510
static gboolean
zathura_page_widget_draw(GtkWidget* widget, cairo_t* cairo)
{
Sebastian Ramacher's avatar
Sebastian Ramacher committed
511 512
  ZathuraPage* page        = ZATHURA_PAGE(widget);
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(page);
513

514
  zathura_document_t* document   = zathura_page_get_document(priv->page);
515 516 517
  const unsigned int page_height = gtk_widget_get_allocated_height(widget);
  const unsigned int page_width  = gtk_widget_get_allocated_width(widget);

518
  if (priv->surface != NULL || priv->thumbnail != NULL) {
519 520
    cairo_save(cairo);

Sebastian Ramacher's avatar
Sebastian Ramacher committed
521
    const unsigned int rotation = zathura_document_get_rotation(document);
522
    switch (rotation) {
523 524 525 526 527 528 529 530 531 532 533
      case 90:
        cairo_translate(cairo, page_width, 0);
        break;
      case 180:
        cairo_translate(cairo, page_width, page_height);
        break;
      case 270:
        cairo_translate(cairo, 0, page_height);
        break;
    }

534 535
    if (rotation != 0) {
      cairo_rotate(cairo, rotation * G_PI / 180.0);
536 537
    }

538 539 540 541 542 543 544
    if (priv->surface != NULL) {
      cairo_set_source_surface(cairo, priv->surface, 0, 0);
      cairo_paint(cairo);
      cairo_restore(cairo);
    } else {
      const unsigned int height = cairo_image_surface_get_height(priv->thumbnail);
      const unsigned int width = cairo_image_surface_get_width(priv->thumbnail);
545 546 547
      unsigned int pheight = (rotation % 180 ? page_width : page_height);
      unsigned int pwidth = (rotation % 180 ? page_height : page_width);

548 549 550 551
      /* note: this always returns 1 and 1 if Cairo too old for device scale API */
      zathura_device_factors_t device = get_safe_device_factors(priv->thumbnail);
      pwidth *= device.x;
      pheight *= device.y;
552 553 554 555 556 557 558 559 560 561 562

      cairo_scale(cairo, pwidth / (double)width, pheight / (double)height);
      cairo_set_source_surface(cairo, priv->thumbnail, 0, 0);
      cairo_pattern_set_extend(cairo_get_source(cairo), CAIRO_EXTEND_PAD);
      if (pwidth < width || pheight < height) {
        /* pixman bilinear downscaling is slow */
        cairo_pattern_set_filter(cairo_get_source(cairo), CAIRO_FILTER_FAST);
      }
      cairo_set_operator(cairo, CAIRO_OPERATOR_SOURCE);
      cairo_paint(cairo);
      cairo_restore(cairo);
563 564
      /* All but the last jobs requested here are aborted during zooming.
       * Processing and aborting smaller jobs first improves responsiveness. */
565
      const gint64 penalty = (gint64)pwidth * (gint64)pheight;
566
      zathura_render_request(priv->render_request, g_get_real_time() + penalty);
567 568
      return FALSE;
    }
569

570 571
    /* draw links */
    set_font_from_property(cairo, priv->zathura, CAIRO_FONT_WEIGHT_BOLD);
572 573 574 575

    float transparency = 0.5;
    girara_setting_get(priv->zathura->ui.session, "highlight-transparency", &transparency);

576
    if (priv->links.draw == true && priv->links.n != 0) {
577
      unsigned int link_counter = 0;
578 579 580
      GIRARA_LIST_FOREACH_BODY(priv->links.list, zathura_link_t*, link,
        if (link != NULL) {
          zathura_rectangle_t rectangle = recalc_rectangle(priv->page, zathura_link_get_position(link));
Moritz Lipp's avatar
Moritz Lipp committed
581

582 583 584 585 586 587 588 589 590 591 592 593 594 595 596
          /* draw position */
          const GdkRGBA color = priv->zathura->ui.colors.highlight_color;
          cairo_set_source_rgba(cairo, color.red, color.green, color.blue, transparency);
          cairo_rectangle(cairo, rectangle.x1, rectangle.y1,
                          (rectangle.x2 - rectangle.x1), (rectangle.y2 - rectangle.y1));
          cairo_fill(cairo);

          /* draw text */
          cairo_set_source_rgba(cairo, 0, 0, 0, 1);
          cairo_move_to(cairo, rectangle.x1 + 1, rectangle.y2 - 1);
          char* link_number = g_strdup_printf("%i", priv->links.offset + ++link_counter);
          cairo_show_text(cairo, link_number);
          g_free(link_number);
        }
      );
597
    }
598 599

    /* draw search results */
600
    if (priv->search.list != NULL && priv->search.draw == true) {
601
      int idx = 0;
602 603
      GIRARA_LIST_FOREACH_BODY(priv->search.list, zathura_rectangle_t*, rect,
        zathura_rectangle_t rectangle = recalc_rectangle(priv->page, *rect);
604

605 606 607 608 609 610 611 612 613 614 615 616 617
        /* draw position */
        if (idx == priv->search.current) {
          const GdkRGBA color = priv->zathura->ui.colors.highlight_color_active;
          cairo_set_source_rgba(cairo, color.red, color.green, color.blue, transparency);
        } else {
          const GdkRGBA color = priv->zathura->ui.colors.highlight_color;
          cairo_set_source_rgba(cairo, color.red, color.green, color.blue, transparency);
        }
        cairo_rectangle(cairo, rectangle.x1, rectangle.y1,
                        (rectangle.x2 - rectangle.x1), (rectangle.y2 - rectangle.y1));
        cairo_fill(cairo);
        ++idx;
      );
618
    }
619
    /* draw selection */
620
    if (priv->mouse.selection.y2 != -1 && priv->mouse.selection.x2 != -1) {
621 622
      const GdkRGBA color = priv->zathura->ui.colors.highlight_color;
      cairo_set_source_rgba(cairo, color.red, color.green, color.blue, transparency);
623
      cairo_rectangle(cairo, priv->mouse.selection.x1, priv->mouse.selection.y1,
Moritz Lipp's avatar
Moritz Lipp committed
624
                      (priv->mouse.selection.x2 - priv->mouse.selection.x1), (priv->mouse.selection.y2 - priv->mouse.selection.y1));
Sebastian Ramacher's avatar
Sebastian Ramacher committed
625
      cairo_fill(cairo);
626
    }
627 628
  } else {
    /* set background color */
629
    if (zathura_renderer_recolor_enabled(priv->zathura->sync.render_thread) == true) {
630
      GdkRGBA color;
631
      zathura_renderer_get_recolor_colors(priv->zathura->sync.render_thread, &color, NULL);
632
      cairo_set_source_rgb(cairo, color.red, color.green, color.blue);
633
    } else {
634 635
      const GdkRGBA color = priv->zathura->ui.colors.render_loading_bg;
      cairo_set_source_rgb(cairo, color.red, color.green, color.blue);
636
    }
637
    cairo_rectangle(cairo, 0, 0, page_width, page_height);
638 639 640 641 642 643 644
    cairo_fill(cairo);

    bool render_loading = true;
    girara_setting_get(priv->zathura->ui.session, "render-loading", &render_loading);

    /* write text */
    if (render_loading == true) {
645
      if (zathura_renderer_recolor_enabled(priv->zathura->sync.render_thread) == true) {
646
        GdkRGBA color;
647
        zathura_renderer_get_recolor_colors(priv->zathura->sync.render_thread, NULL, &color);
648
        cairo_set_source_rgb(cairo, color.red, color.green, color.blue);
649
      } else {
650
        const GdkRGBA color = priv->zathura->ui.colors.render_loading_fg;
651
        cairo_set_source_rgb(cairo, color.red, color.green, color.blue);
652 653
      }

654
      const char* text = _("Loading...");
655 656 657 658
      cairo_select_font_face(cairo, "Sans", CAIRO_FONT_SLANT_NORMAL, CAIRO_FONT_WEIGHT_NORMAL);
      cairo_set_font_size(cairo, 16.0);
      cairo_text_extents_t extents;
      cairo_text_extents(cairo, text, &extents);
659 660
      double x = page_width * 0.5 - (extents.width * 0.5 + extents.x_bearing);
      double y = page_height * 0.5 - (extents.height * 0.5 + extents.y_bearing);
661 662 663 664 665
      cairo_move_to(cairo, x, y);
      cairo_show_text(cairo, text);
    }

    /* render real page */
666
    zathura_render_request(priv->render_request, g_get_real_time());
667 668 669 670 671
  }
  return FALSE;
}

static void
672
zathura_page_widget_redraw_canvas(ZathuraPage* pageview)
673 674 675 676 677
{
  GtkWidget* widget = GTK_WIDGET(pageview);
  gtk_widget_queue_draw(widget);
}

678
/* smaller than max to be replaced by actual renders */
679
#define THUMBNAIL_INITIAL_ZOOM 0.5
680
/* small enough to make bilinear downscaling fast */
681
#define THUMBNAIL_MAX_ZOOM 0.5
682 683

static bool
684
surface_small_enough(cairo_surface_t* surface, size_t max_size, cairo_surface_t* old)
685
{
Sebastian Ramacher's avatar
Sebastian Ramacher committed
686
  if (cairo_surface_get_type(surface) != CAIRO_SURFACE_TYPE_IMAGE) {
687
    return true;
Sebastian Ramacher's avatar
Sebastian Ramacher committed
688
  }
689 690 691

  const unsigned int width = cairo_image_surface_get_width(surface);
  const unsigned int height = cairo_image_surface_get_height(surface);
692
  const size_t new_size = width * height;
693
  if (new_size > max_size) {
694
    return false;
Sebastian Ramacher's avatar
Sebastian Ramacher committed
695
  }
696 697 698 699

  if (old != NULL) {
    const unsigned int width_old = cairo_image_surface_get_width(old);
    const unsigned int height_old = cairo_image_surface_get_height(old);
700
    const size_t old_size = width_old * height_old;
701
    if (new_size < old_size && new_size >= old_size * THUMBNAIL_MAX_ZOOM * THUMBNAIL_MAX_ZOOM) {
702
      return false;
Sebastian Ramacher's avatar
Sebastian Ramacher committed
703
    }
704 705 706 707 708 709
  }

  return true;
}

static cairo_surface_t *
710
draw_thumbnail_image(cairo_surface_t* surface, size_t max_size)
711 712 713
{
  unsigned int width = cairo_image_surface_get_width(surface);
  unsigned int height = cairo_image_surface_get_height(surface);
714 715 716
  double scale = sqrt((double)max_size / (width * height)) * THUMBNAIL_INITIAL_ZOOM;
  if (scale > THUMBNAIL_MAX_ZOOM) {
    scale = THUMBNAIL_MAX_ZOOM;
Sebastian Ramacher's avatar
Sebastian Ramacher committed
717
  }
718 719 720
  width = width * scale;
  height = height * scale;

721 722
  /* note: this always returns 1 and 1 if Cairo too old for device scale API */
  zathura_device_factors_t device = get_safe_device_factors(surface);
723 724
  const unsigned int unscaled_width = width / device.x;
  const unsigned int unscaled_height = height / device.y;
725

726
  /* create thumbnail surface, taking width and height as _unscaled_ device units */
727
  cairo_surface_t *thumbnail;
728
  thumbnail = cairo_surface_create_similar(surface, CAIRO_CONTENT_COLOR, unscaled_width, unscaled_height);
Lingzhu Xiang's avatar
Lingzhu Xiang committed
729 730 731
  if (thumbnail == NULL) {
    return NULL;
  }
732
  cairo_t *cr = cairo_create(thumbnail);
Lingzhu Xiang's avatar
Lingzhu Xiang committed
733 734 735 736
  if (cr == NULL) {
    cairo_surface_destroy(thumbnail);
    return NULL;
  }
737 738 739 740 741 742 743 744 745 746 747

  cairo_scale(cr, scale, scale);
  cairo_set_source_surface(cr, surface, 0, 0);
  cairo_pattern_set_filter(cairo_get_source(cr), CAIRO_FILTER_BILINEAR);
  cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE);
  cairo_paint(cr);
  cairo_destroy(cr);

  return thumbnail;
}

748
void
749
zathura_page_widget_update_surface(ZathuraPage* widget, cairo_surface_t* surface, bool keep_thumbnail)
750
{
Sebastian Ramacher's avatar
Sebastian Ramacher committed
751
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
752 753 754 755 756
  int thumbnail_size = 0;
  girara_setting_get(priv->zathura->ui.session, "page-thumbnail-size", &thumbnail_size);
  if (thumbnail_size <= 0) {
    thumbnail_size = ZATHURA_PAGE_THUMBNAIL_DEFAULT_SIZE;
  }
757 758
  bool new_render = (priv->surface == NULL && priv->thumbnail == NULL);

Moritz Lipp's avatar
Moritz Lipp committed
759
  if (priv->surface != NULL) {
760
    cairo_surface_destroy(priv->surface);
761
    priv->surface = NULL;
762
  }
763
  if (surface != NULL) {
764 765
    priv->surface = surface;
    cairo_surface_reference(surface);
766

767
    if (surface_small_enough(surface, thumbnail_size, priv->thumbnail)) {
Sebastian Ramacher's avatar
Sebastian Ramacher committed
768
      if (priv->thumbnail != NULL) {
769
        cairo_surface_destroy(priv->thumbnail);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
770
      }
771 772 773
      priv->thumbnail = surface;
      cairo_surface_reference(surface);
    } else if (new_render) {
774
      priv->thumbnail = draw_thumbnail_image(surface, thumbnail_size);
775 776 777 778
    }
  } else if (!keep_thumbnail && priv->thumbnail != NULL) {
    cairo_surface_destroy(priv->thumbnail);
    priv->thumbnail = NULL;
779
  }
780
  /* force a redraw here */
781 782 783
  if (priv->surface != NULL) {
    zathura_page_widget_redraw_canvas(widget);
  }
784 785
}

786
static void
787 788
cb_update_surface(ZathuraRenderRequest* UNUSED(request),
    cairo_surface_t* surface, void* data)
789 790 791
{
  ZathuraPage* widget = data;
  g_return_if_fail(ZATHURA_IS_PAGE(widget));
792
  zathura_page_widget_update_surface(widget, surface, false);
793 794
}

795 796 797 798 799 800
static void
cb_cache_added(ZathuraRenderRequest* UNUSED(request), void* data)
{
  ZathuraPage* widget = data;
  g_return_if_fail(ZATHURA_IS_PAGE(widget));

Sebastian Ramacher's avatar
Sebastian Ramacher committed
801
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
802 803 804 805 806 807 808 809 810
  priv->cached = true;
}

static void
cb_cache_invalidated(ZathuraRenderRequest* UNUSED(request), void* data)
{
  ZathuraPage* widget = data;
  g_return_if_fail(ZATHURA_IS_PAGE(widget));

Sebastian Ramacher's avatar
Sebastian Ramacher committed
811
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
812 813 814 815 816
  if (zathura_page_widget_have_surface(widget) == true &&
      priv->cached == true &&
      zathura_page_get_visibility(priv->page) == false) {
    /* The page was in the cache but got removed and is invisible, so get rid of
     * the surface. */
817
    zathura_page_widget_update_surface(widget, NULL, false);
818 819 820 821
  }
  priv->cached = false;
}

822
static void
823
zathura_page_widget_size_allocate(GtkWidget* widget, GdkRectangle* allocation)
824
{
825
  GTK_WIDGET_CLASS(zathura_page_widget_parent_class)->size_allocate(widget, allocation);
826 827 828

  ZathuraPage* page = ZATHURA_PAGE(widget);
  zathura_page_widget_abort_render_request(page);
829
  zathura_page_widget_update_surface(page, NULL, true);
830
}
831 832

static void
833
redraw_rect(ZathuraPage* widget, zathura_rectangle_t* rectangle)
834
{
Moritz Lipp's avatar
Moritz Lipp committed
835
  /* cause the rect to be drawn */
836
  GdkRectangle grect;
837
  grect.x = rectangle->x1;
838
  grect.y = rectangle->y1;
839 840
  grect.width  = (rectangle->x2 + 1) - rectangle->x1;
  grect.height = (rectangle->y2 + 1) - rectangle->y1;
841
  gtk_widget_queue_draw_area(GTK_WIDGET(widget), grect.x, grect.y, grect.width, grect.height);
842 843
}

844
static void
845
redraw_all_rects(ZathuraPage* widget, girara_list_t* rectangles)
846
{
Sebastian Ramacher's avatar
Sebastian Ramacher committed
847
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
848

849
  GIRARA_LIST_FOREACH_BODY(rectangles, zathura_rectangle_t*, rect,
850 851
    zathura_rectangle_t rectangle = recalc_rectangle(priv->page, *rect);
    redraw_rect(widget, &rectangle);
852
  );
853 854
}

855
zathura_link_t*
856
zathura_page_widget_link_get(ZathuraPage* widget, unsigned int index)
857
{
858
  g_return_val_if_fail(widget != NULL, NULL);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
859
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(widget);
860
  g_return_val_if_fail(priv != NULL, NULL);
861

862 863 864
  if (priv->links.list != NULL && index >= priv->links.offset &&
      girara_list_size(priv->links.list) > index - priv->links.offset) {
    return girara_list_nth(priv->links.list, index - priv->links.offset);
865 866 867
  } else {
    return NULL;
  }
868
}
Moritz Lipp's avatar
Moritz Lipp committed
869

870 871
static gboolean
cb_zathura_page_widget_button_press_event(GtkWidget* widget, GdkEventButton* button)
Moritz Lipp's avatar
Moritz Lipp committed
872 873 874 875
{
  g_return_val_if_fail(widget != NULL, false);
  g_return_val_if_fail(button != NULL, false);

Sebastian Ramacher's avatar
Sebastian Ramacher committed
876 877
  ZathuraPage* page        = ZATHURA_PAGE(widget);
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(page);
Moritz Lipp's avatar
Moritz Lipp committed
878

879 880 881 882
  if (girara_callback_view_button_press_event(widget, button, priv->zathura->ui.session) == true) {
    return true;
  }

883
  if (button->button == GDK_BUTTON_PRIMARY) { /* left click */
Moritz Lipp's avatar
Moritz Lipp committed
884 885
    if (button->type == GDK_BUTTON_PRESS) {
      /* start the selection */
886 887 888 889 890 891 892
      priv->mouse.selection_basepoint.x = button->x;
      priv->mouse.selection_basepoint.y = button->y;

      priv->mouse.selection.x1 = button->x;
      priv->mouse.selection.y1 = button->y;
      priv->mouse.selection.x2 = button->x;
      priv->mouse.selection.y2 = button->y;
Moritz Lipp's avatar
Moritz Lipp committed
893 894
    } else if (button->type == GDK_2BUTTON_PRESS || button->type == GDK_3BUTTON_PRESS) {
      /* abort the selection */
895 896 897 898 899 900 901
      priv->mouse.selection_basepoint.x = -1;
      priv->mouse.selection_basepoint.y = -1;

      priv->mouse.selection.x1 = -1;
      priv->mouse.selection.y1 = -1;
      priv->mouse.selection.x2 = -1;
      priv->mouse.selection.y2 = -1;
Moritz Lipp's avatar
Moritz Lipp committed
902 903 904
    }

    return true;
905
  } else if (gdk_event_triggers_context_menu((GdkEvent*) button) == TRUE && button->type == GDK_BUTTON_PRESS) { /* right click */
Moritz Lipp's avatar
Moritz Lipp committed
906 907
    zathura_page_widget_popup_menu(widget, button);
    return true;
908
  }
Moritz Lipp's avatar
Moritz Lipp committed
909

910 911 912 913 914 915 916 917
  return false;
}

static gboolean
cb_zathura_page_widget_button_release_event(GtkWidget* widget, GdkEventButton* button)
{
  g_return_val_if_fail(widget != NULL, false);
  g_return_val_if_fail(button != NULL, false);
918 919

  if (button->type != GDK_BUTTON_RELEASE) {
920 921 922
    return false;
  }

923 924 925
  const int oldx = button->x;
  const int oldy = button->y;

Sebastian Ramacher's avatar
Sebastian Ramacher committed
926 927
  ZathuraPage* page        = ZATHURA_PAGE(widget);
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(page);
928

Sebastian Ramacher's avatar
Sebastian Ramacher committed
929 930
  zathura_document_t* document = zathura_page_get_document(priv->page);
  const double scale           = zathura_document_get_scale(document);
931 932 933 934

  button->x /= scale;
  button->y /= scale;

Sebastian Ramacher's avatar
Sebastian Ramacher committed
935
  g_signal_emit(page, signals[BUTTON_RELEASE], 0, button);
936 937 938 939

  button->x = oldx;
  button->y = oldy;

940
  if (button->button != GDK_BUTTON_PRIMARY) {
941 942 943
    return false;
  }

944
  if (priv->mouse.selection.y2 == -1 && priv->mouse.selection.x2 == -1 ) {
945
    /* simple single click */
Moritz Lipp's avatar
Moritz Lipp committed
946
    /* get links */
947 948 949 950
    if (priv->links.retrieved == false) {
      priv->links.list      = zathura_page_links_get(priv->page, NULL);
      priv->links.retrieved = true;
      priv->links.n         = (priv->links.list == NULL) ? 0 : girara_list_size(priv->links.list);
Moritz Lipp's avatar
Moritz Lipp committed
951 952
    }

953
    if (priv->links.list != NULL && priv->links.n > 0) {
954 955
      GIRARA_LIST_FOREACH_BODY(priv->links.list, zathura_link_t*, link,
        const zathura_rectangle_t rect = recalc_rectangle(priv->page, zathura_link_get_position(link));
956 957
        if (rect.x1 <= oldx && rect.x2 >= oldx
            && rect.y1 <= oldy && rect.y2 >= oldy) {
958
          zathura_link_evaluate(priv->zathura, link);
959
          break;
960 961
        }
      );
Moritz Lipp's avatar
Moritz Lipp committed
962
    }
963
  } else {
964
    redraw_rect(ZATHURA_PAGE(widget), &priv->mouse.selection);
Moritz Lipp's avatar
Moritz Lipp committed
965

966
    zathura_rectangle_t tmp = priv->mouse.selection;
Moritz Lipp's avatar
Moritz Lipp committed
967

968 969 970 971
    tmp.x1 /= scale;
    tmp.x2 /= scale;
    tmp.y1 /= scale;
    tmp.y2 /= scale;
972

973
    char* text = zathura_page_get_text(priv->page, tmp, NULL);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
974 975 976
    if (text != NULL && *text != '\0') {
      /* emit text-selected signal */
      g_signal_emit(ZATHURA_PAGE(widget), signals[TEXT_SELECTED], 0, text);
977
    }
Sebastian Ramacher's avatar
Sebastian Ramacher committed
978
    g_free(text);
979
  }
Moritz Lipp's avatar
Moritz Lipp committed
980

981 982 983 984 985 986 987
  priv->mouse.selection_basepoint.x = -1;
  priv->mouse.selection_basepoint.y = -1;

  priv->mouse.selection.x1 = -1;
  priv->mouse.selection.y1 = -1;
  priv->mouse.selection.x2 = -1;
  priv->mouse.selection.y2 = -1;
988

989 990 991 992 993 994 995 996
  return false;
}

static gboolean
cb_zathura_page_widget_motion_notify(GtkWidget* widget, GdkEventMotion* event)
{
  g_return_val_if_fail(widget != NULL, false);
  g_return_val_if_fail(event != NULL, false);
997

Sebastian Ramacher's avatar
Sebastian Ramacher committed
998 999 1000
  ZathuraPage* page        = ZATHURA_PAGE(widget);
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(page);

1001
  if ((event->state & GDK_BUTTON1_MASK) == 0) {
1002 1003 1004 1005 1006 1007 1008 1009
    if (priv->links.retrieved == false) {
      priv->links.list      = zathura_page_links_get(priv->page, NULL);
      priv->links.retrieved = true;
      priv->links.n         = (priv->links.list == NULL) ? 0 : girara_list_size(priv->links.list);
    }

    if (priv->links.list != NULL && priv->links.n > 0) {
      bool over_link = false;
1010
      GIRARA_LIST_FOREACH_BODY(priv->links.list, zathura_link_t*, link,
1011 1012 1013
        zathura_rectangle_t rect = recalc_rectangle(priv->page, zathura_link_get_position(link));
        if (rect.x1 <= event->x && rect.x2 >= event->x && rect.y1 <= event->y && rect.y2 >= event->y) {
          over_link = true;
1014
          break;
1015
        }
1016
      );
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027

      if (priv->mouse.over_link != over_link) {
        if (over_link == true) {
          g_signal_emit(ZATHURA_PAGE(widget), signals[ENTER_LINK], 0);
        } else {
          g_signal_emit(ZATHURA_PAGE(widget), signals[LEAVE_LINK], 0);
        }
        priv->mouse.over_link = over_link;
      }
    }

1028 1029 1030
    return false;
  }

1031 1032
  zathura_rectangle_t tmp = priv->mouse.selection;
  if (event->x < priv->mouse.selection_basepoint.x) {
1033
    tmp.x1 = event->x;
1034
    tmp.x2 = priv->mouse.selection_basepoint.x;
1035 1036
  } else {
    tmp.x2 = event->x;
1037
    tmp.x1 = priv->mouse.selection_basepoint.x;
1038
  }
1039
  if (event->y < priv->mouse.selection_basepoint.y) {
1040
    tmp.y1 = event->y;
1041
    tmp.y2 = priv->mouse.selection_basepoint.y;
1042
  } else {
1043
    tmp.y1 = priv->mouse.selection_basepoint.y;
1044
    tmp.y2 = event->y;
Moritz Lipp's avatar
Moritz Lipp committed
1045 1046
  }

1047
  redraw_rect(ZATHURA_PAGE(widget), &priv->mouse.selection);
1048
  redraw_rect(ZATHURA_PAGE(widget), &tmp);
1049
  priv->mouse.selection = tmp;
1050

Moritz Lipp's avatar
Moritz Lipp committed
1051 1052
  return false;
}
Moritz Lipp's avatar
Moritz Lipp committed
1053

1054 1055 1056 1057 1058
static gboolean
cb_zathura_page_widget_leave_notify(GtkWidget* widget, GdkEventCrossing* UNUSED(event))
{
  g_return_val_if_fail(widget != NULL, false);

Sebastian Ramacher's avatar
Sebastian Ramacher committed
1059 1060
  ZathuraPage* page        = ZATHURA_PAGE(widget);
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(page);
1061 1062 1063 1064 1065 1066 1067
  if (priv->mouse.over_link == true) {
    g_signal_emit(ZATHURA_PAGE(widget), signals[LEAVE_LINK], 0);
    priv->mouse.over_link = false;
  }
  return false;
}

Moritz Lipp's avatar
Moritz Lipp committed
1068 1069 1070 1071
static void
zathura_page_widget_popup_menu(GtkWidget* widget, GdkEventButton* event)
{
  g_return_if_fail(widget != NULL);
1072 1073 1074 1075 1076
  if (event == NULL) {
    /* do something here in the future in case we have general popups */
    return;
  }

Sebastian Ramacher's avatar
Sebastian Ramacher committed
1077 1078
  ZathuraPage* page        = ZATHURA_PAGE(widget);
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(page);
Moritz Lipp's avatar
Moritz Lipp committed
1079

1080 1081 1082
  if (priv->images.retrieved == false) {
    priv->images.list      = zathura_page_images_get(priv->page, NULL);
    priv->images.retrieved = true;
Moritz Lipp's avatar
Moritz Lipp committed
1083 1084
  }

1085
  if (priv->images.list == NULL) {
Moritz Lipp's avatar
Moritz Lipp committed
1086 1087 1088 1089 1090
    return;
  }

  /* search for underlaying image */
  zathura_image_t* image = NULL;
1091
  GIRARA_LIST_FOREACH_BODY(priv->images.list, zathura_image_t*, image_it,
1092 1093 1094 1095
    zathura_rectangle_t rect = recalc_rectangle(priv->page, image_it->position);
    if (rect.x1 <= event->x && rect.x2 >= event->x && rect.y1 <= event->y && rect.y2 >= event->y) {
      image = image_it;
    }
1096
  );
Moritz Lipp's avatar
Moritz Lipp committed
1097 1098 1099 1100 1101

  if (image == NULL) {
    return;
  }

1102
  priv->images.current = image;
Moritz Lipp's avatar
Moritz Lipp committed
1103 1104 1105 1106 1107 1108 1109 1110 1111

  /* setup menu */
  GtkWidget* menu = gtk_menu_new();

  typedef struct menu_item_s {
    char* text;
    void (*callback)(GtkMenuItem*, ZathuraPage*);
  } menu_item_t;

1112
  const menu_item_t menu_items[] = {
1113 1114
    { _("Copy image"),    cb_menu_image_copy },
    { _("Save image as"), cb_menu_image_save },
Moritz Lipp's avatar
Moritz Lipp committed
1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125
  };

  for (unsigned int i = 0; i < LENGTH(menu_items); i++) {
    GtkWidget* item = gtk_menu_item_new_with_label(menu_items[i].text);
    gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
    gtk_widget_show(item);
    g_signal_connect(G_OBJECT(item), "activate", G_CALLBACK(menu_items[i].callback), ZATHURA_PAGE(widget));
  }

  /* attach and popup */
  gtk_menu_attach_to_widget(GTK_MENU(menu), widget, NULL);
1126 1127 1128
#if GTK_CHECK_VERSION(3, 22, 0)
  gtk_menu_popup_at_pointer(GTK_MENU(menu), (GdkEvent*) event);
#else
1129
  gtk_menu_popup(GTK_MENU(menu), NULL, NULL, NULL, NULL, event->button, event->time);
1130
#endif
Moritz Lipp's avatar
Moritz Lipp committed
1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145
}

static gboolean
cb_zathura_page_widget_popup_menu(GtkWidget* widget)
{
  zathura_page_widget_popup_menu(widget, NULL);

  return TRUE;
}

static void
cb_menu_image_copy(GtkMenuItem* item, ZathuraPage* page)
{
  g_return_if_fail(item != NULL);
  g_return_if_fail(page != NULL);
Sebastian Ramacher's avatar
Sebastian Ramacher committed
1146
  ZathuraPagePrivate* priv = zathura_page_widget_get_instance_private(page);
1147
  g_return_if_fail(priv->images.current != NULL);
Moritz Lipp's avatar
Moritz Lipp committed
1148

1149
  cairo_surface_t* surface = zathura_page_image_get_cairo(priv->page, priv->images.current, NULL);
Moritz Lipp's avatar
Moritz Lipp committed
1150 1151 1152 1153
  if (surface == NULL) {
    return;
  }

1154 1155
  const int width  = cairo_image_surface_get_width(surface);
  const int height = cairo_image_surface_get_height(surface);
Moritz Lipp's avatar
Moritz Lipp committed
1156

1157
  GdkPixbuf* pixbuf = gdk_pixbuf_get_from_surface(surface, 0, 0, width, height);
1158 1159
  g_signal_emit(page, signals[IMAGE_SELECTED], 0, pixbuf);
  g_object_unref(pixbuf);
1160
  cairo_surface_destroy(surface);
Moritz Lipp's avatar
Moritz Lipp committed
1161

1162
  /* reset */
1163
  priv->images.current = NULL;
Moritz Lipp's avatar
Moritz Lipp committed
1164
}
1165

1166 1167 1168 1169 1170
static void
cb_menu_image_save(GtkMenuItem* item, ZathuraPage* page)
{
  g_return_if_fail(item != NULL);
  g_return_if_fail(page != NULL);
Sebastian Ramacher's avatar