ef2e3d3dbbb64fa437d4fde7fedadc6a711c822b
[spectmorph.git] / glui / smwindow.cc
1 // Licensed GNU LGPL v3 or later: http://www.gnu.org/licenses/lgpl.html
2
3 #include "pugl/gl.h"
4 #if !__APPLE__
5 #include "GL/glext.h"
6 #endif
7 #include "smwindow.hh"
8 #include "smmenubar.hh"
9 #include "smscrollview.hh"
10 #include "smnativefiledialog.hh"
11 #include "smconfig.hh"
12 #include "smtimer.hh"
13 #include "smshortcut.hh"
14 #include "pugl/cairo_gl.h"
15 #include <string.h>
16 #include <unistd.h>
17 #include <glib.h>
18 #include <sys/time.h>
19
20 using namespace SpectMorph;
21
22 using std::vector;
23 using std::string;
24 using std::min;
25 using std::max;
26
27 struct SpectMorph::CairoGL
28 {
29 private:
30   PuglCairoGL pugl_cairo_gl;
31   cairo_surface_t *surface;
32   int  m_width;
33   int  m_height;
34
35   vector<uint32> tmp_buffer;
36
37 public:
38   cairo_t *cr;
39
40   CairoGL (int width, int height) :
41     m_width (width), m_height (height)
42   {
43     memset (&pugl_cairo_gl, 0, sizeof (pugl_cairo_gl));
44
45     surface = pugl_cairo_gl_create (&pugl_cairo_gl, width, height, 4);
46     cr = cairo_create (surface);
47   }
48   ~CairoGL()
49   {
50     cairo_destroy (cr);
51     cairo_surface_destroy (surface);
52
53     pugl_cairo_gl_free (&pugl_cairo_gl);
54   }
55   void
56   configure()
57   {
58     pugl_cairo_gl_configure (&pugl_cairo_gl, m_width, m_height);
59
60     glTexImage2D (GL_TEXTURE_RECTANGLE_ARB, 0, GL_RGBA8,
61                   m_width, m_height, 0,
62                   GL_BGRA, GL_UNSIGNED_BYTE, pugl_cairo_gl.buffer);
63   }
64   void
65   draw (int x, int y, int w, int h)
66   {
67     (void) pugl_cairo_gl_draw; // reimplement this:
68
69     glMatrixMode(GL_MODELVIEW);
70     glLoadIdentity();
71     glViewport(0, 0, m_width, m_height);
72     glClear(GL_COLOR_BUFFER_BIT);
73
74     glPushMatrix();
75     glEnable(GL_TEXTURE_RECTANGLE_ARB);
76     glEnable(GL_TEXTURE_2D);
77
78     void *draw_buffer;
79     if (x == 0 && y == 0 && w == m_width && h == m_height)
80       {
81         // draw full frame
82         draw_buffer = pugl_cairo_gl.buffer;
83       }
84     else
85       {
86         uint32 *src_buffer = reinterpret_cast<uint32 *> (pugl_cairo_gl.buffer);
87         tmp_buffer.resize (w * h);
88
89         for (int by = 0; by < h; by++)
90           {
91             memcpy (&tmp_buffer[by * w], &src_buffer[(by + y) * m_width + x], w * 4);
92           }
93         draw_buffer = tmp_buffer.data();
94       }
95
96     glTexSubImage2D (GL_TEXTURE_RECTANGLE_ARB, 0,
97                      x, y, w, h,
98                      GL_BGRA, GL_UNSIGNED_BYTE, draw_buffer);
99
100     glBegin(GL_QUADS);
101     glTexCoord2f(0.0f, (GLfloat)m_height);
102     glVertex2f(-1.0f, -1.0f);
103
104     glTexCoord2f((GLfloat)m_width, (GLfloat)m_height);
105     glVertex2f(1.0f, -1.0f);
106
107     glTexCoord2f((GLfloat)m_width, 0.0f);
108     glVertex2f(1.0f, 1.0f);
109
110     glTexCoord2f(0.0f, 0.0f);
111     glVertex2f(-1.0f, 1.0f);
112     glEnd();
113
114     glDisable(GL_TEXTURE_2D);
115     glDisable(GL_TEXTURE_RECTANGLE_ARB);
116     glPopMatrix();
117   }
118   int
119   width()
120   {
121     return m_width;
122   }
123   int
124   height()
125   {
126     return m_height;
127   }
128 };
129
130 static void
131 on_event (PuglView* view, const PuglEvent* event)
132 {
133   Window *window = reinterpret_cast<Window *> (puglGetHandle (view));
134
135   window->on_event (event);
136 }
137
138 static void
139 on_resize (PuglView *view, int *width, int *height, int *set_hints)
140 {
141   Window *window = reinterpret_cast<Window *> (puglGetHandle (view));
142
143   window->on_resize (width, height);
144 }
145
146 Window::Window (const string& title, int width, int height, PuglNativeWindow win_id, bool resize, PuglNativeWindow transient_parent) :
147   Widget (nullptr, 0, 0, width, height),
148   draw_grid (false)
149 {
150   Config cfg;
151
152   global_scale = cfg.zoom() / 100.0;
153   auto_redraw = false;
154
155   view = puglInit (nullptr, nullptr);
156
157   /* draw 128 bits from random generator to ensure that window class name is unique */
158   string window_class = "SpectMorph_";
159   for (size_t i = 0; i < 4; i++)
160     window_class += string_printf ("%08x", g_random_int());
161
162   int scaled_width, scaled_height;
163   get_scaled_size (&scaled_width, &scaled_height);
164
165   puglInitWindowClass (view, window_class.c_str());
166   puglInitWindowSize (view, scaled_width, scaled_height);
167   //puglInitWindowMinSize (view, 256, 256);
168   puglInitResizable (view, resize);
169   puglIgnoreKeyRepeat (view, true);
170   if (win_id)
171     puglInitWindowParent (view, win_id);
172   if (transient_parent)
173     puglInitTransientFor (view, transient_parent);
174   puglCreateWindow (view, title.c_str());
175
176   puglSetHandle (view, this);
177   puglSetEventFunc (view, ::on_event);
178   puglSetResizeFunc (view, ::on_resize);
179
180   cairo_gl.reset (new CairoGL (scaled_width, scaled_height));
181   update_full();
182
183   puglEnterContext (view);
184   glEnable (GL_DEPTH_TEST);
185   glDepthFunc (GL_LESS);
186   glClearColor (0.4f, 0.4f, 0.4f, 1.0f);
187   cairo_gl->configure();
188   // printf ("OpenGL Version: %s\n",(const char*) glGetString(GL_VERSION));
189   puglLeaveContext(view, false);
190
191   set_background_color (ThemeColor::WINDOW_BG);
192 }
193
194 Window::~Window()
195 {
196   puglDestroy (view);
197
198   /* this code needs to work if remove_timer & add_timer are called from one of the destructors */
199   for (size_t i = 0; i < timers.size(); i++)
200     {
201       if (timers[i])
202         delete timers[i];
203     }
204   for (size_t i = 0; i < timers.size(); i++)
205     assert (timers[i] == nullptr);
206
207   /* cleanup shortcuts: this code needs to work if remove_shortcut & add_shortcut are called from one of the destructors */
208   for (size_t i = 0; i < shortcuts.size(); i++)
209     {
210       if (shortcuts[i])
211         delete shortcuts[i];
212     }
213   for (size_t i = 0; i < shortcuts.size(); i++)
214     assert (shortcuts[i] == nullptr);
215
216   /* cleanup child windows */
217   for (size_t i = 0; i < child_windows.size(); i++)
218     {
219       if (child_windows[i])
220         delete child_windows[i];
221     }
222 }
223
224 void
225 Window::on_widget_deleted (Widget *child)
226 {
227   /* cheap weak pointer emulation */
228   if (mouse_widget == child)
229     mouse_widget = nullptr;
230   if (enter_widget == child)
231     enter_widget = nullptr;
232   if (menu_widget == child)
233     menu_widget = nullptr;
234   if (keyboard_focus_widget == child)
235     keyboard_focus_widget = nullptr;
236   if (dialog_widget == child)
237     {
238       update_full();
239       dialog_widget = nullptr;
240     }
241 }
242
243 static vector<Widget *>
244 crawl_widgets (const vector<Widget *>& widgets)
245 {
246   vector<Widget *> all_widgets;
247
248   for (auto w : widgets)
249     {
250       all_widgets.push_back (w);
251       auto c_result = crawl_widgets (w->children);
252       all_widgets.insert (all_widgets.end(), c_result.begin(), c_result.end());
253     }
254   return all_widgets;
255 }
256
257 vector<Widget *>
258 Window::crawl_widgets()
259 {
260   return ::crawl_widgets ({ this });
261 }
262
263 static int
264 get_layer (Widget *w, Widget *menu_widget, Widget *dialog_widget)
265 {
266   if (w == menu_widget)
267     return 1;
268   if (w == dialog_widget)
269     return 2;
270
271   if (w->parent)
272     return get_layer (w->parent, menu_widget, dialog_widget);
273   else
274     return 0; // no parent
275 }
276
277 static bool
278 get_visible_recursive (Widget *w)
279 {
280   if (!w->visible())
281     return false;
282
283   if (w->parent)
284     return get_visible_recursive (w->parent);
285   else
286     return true; // no parent
287 }
288
289 namespace {
290
291 struct IRect
292 {
293   int x, y, w, h;
294
295   IRect (const Rect& r, double scale)
296   {
297     x = r.x() * scale;
298     y = r.y() * scale;
299     w = r.width() * scale;
300     h = r.height() * scale;
301   }
302
303   void
304   grow (int px)
305   {
306     x -= px;
307     y -= px;
308     w += px * 2;
309     h += px * 2;
310   }
311
312   void
313   clip (int width, int height)
314   {
315     x = sm_bound (0, x, width);
316     y = sm_bound (0, y, height);
317     w = sm_bound (0, w, width - x);
318     h = sm_bound (0, h, height - y);
319   }
320 };
321
322 };
323
324 void
325 Window::on_display()
326 {
327   // glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
328
329   cairo_save (cairo_gl->cr);
330
331   /* for debugging, we want a rectangle around the area we would normally update */
332   const bool draw_update_region_rect = debug_update_region && !update_full_redraw;
333   if (draw_update_region_rect)
334     update_full_redraw = true;   // always draw full frames in debug mode
335
336   Rect update_region_larger;
337   if (!update_full_redraw)
338     {
339       // setup clipping - only need to redraw part of the screen
340       IRect r (update_region, global_scale);
341
342       // since we have to convert double coordinates to integer, we add a bit of extra space
343       r.grow (4);
344
345       cairo_rectangle (cairo_gl->cr, r.x, r.y, r.w, r.h);
346       cairo_clip (cairo_gl->cr);
347
348       update_region_larger = Rect (r.x / global_scale, r.y / global_scale, r.w / global_scale, r.h / global_scale);
349     }
350
351   for (int layer = 0; layer < 3; layer++)
352     {
353       if (dialog_widget && layer == 2) /* draw rest of ui darker if dialog is open */
354         {
355           cairo_rectangle (cairo_gl->cr, 0, 0, width * global_scale, height * global_scale);
356           cairo_set_source_rgba (cairo_gl->cr, 0.0, 0, 0, 0.5);
357           cairo_fill (cairo_gl->cr);
358         }
359       for (auto w : crawl_widgets())
360         {
361           if (get_layer (w, menu_widget, dialog_widget) == layer && get_visible_recursive (w))
362             {
363               Rect visible_rect = w->abs_visible_rect();
364               if (!update_full_redraw)
365                 {
366                   // only redraw changed parts
367                   visible_rect = visible_rect.intersection (update_region_larger);
368                 }
369               if (!visible_rect.empty())
370                 {
371                   cairo_t *cr = cairo_gl->cr;
372
373                   cairo_save (cr);
374                   cairo_scale (cr, global_scale, global_scale);
375
376                   DrawEvent devent;
377
378                   // local coordinates
379                   cairo_translate (cr, w->abs_x(), w->abs_y());
380                   if (w->clipping())
381                     {
382                       // translate to widget local coordinates
383                       visible_rect.move_to (visible_rect.x() - w->abs_x(), visible_rect.y() - w->abs_y());
384
385                       cairo_rectangle (cr, visible_rect.x(), visible_rect.y(), visible_rect.width(), visible_rect.height());
386                       cairo_clip (cr);
387
388                       devent.rect = visible_rect;
389                     }
390
391                   if (draw_grid && w == enter_widget)
392                     w->debug_fill (cr);
393
394                   devent.cr = cr;
395                   w->draw (devent);
396                   cairo_restore (cr);
397                 }
398             }
399         }
400     }
401
402   if (have_file_dialog || have_popup_window)
403     {
404       cairo_rectangle (cairo_gl->cr, 0, 0, width * global_scale, height * global_scale);
405       cairo_set_source_rgba (cairo_gl->cr, 0.0, 0, 0, 0.5);
406       cairo_fill (cairo_gl->cr);
407     }
408
409   if (draw_grid)
410     {
411       cairo_t *cr = cairo_gl->cr;
412
413       cairo_save (cr);
414       cairo_scale (cr, global_scale, global_scale);
415
416       cairo_set_source_rgb (cr, 0, 0, 0);
417       cairo_set_line_width (cr, 0.5);
418
419       for (double x = 8; x < width; x += 8)
420         {
421           for (double y = 8; y < height; y += 8)
422             {
423               cairo_move_to (cr, x - 2, y);
424               cairo_line_to (cr, x + 2, y);
425               cairo_move_to (cr, x, y - 2);
426               cairo_line_to (cr, x, y + 2);
427             }
428         }
429       cairo_stroke (cr);
430       cairo_restore (cr);
431     }
432   cairo_restore (cairo_gl->cr);
433
434   if (draw_update_region_rect)
435     {
436       cairo_t *cr = cairo_gl->cr;
437
438       cairo_save (cr);
439       cairo_scale (cr, global_scale, global_scale);
440       cairo_rectangle (cr, update_region.x(), update_region.y(), update_region.width(), update_region.height());
441       cairo_set_source_rgb (cr, 1.0, 0.4, 0.4);
442       cairo_set_line_width (cr, 3.0);
443       cairo_stroke (cr);
444       cairo_restore (cr);
445     }
446
447   if (update_full_redraw)
448     {
449       cairo_gl->draw (0, 0, cairo_gl->width(), cairo_gl->height());
450     }
451   else
452     {
453       IRect draw_region (update_region, global_scale);
454
455       /* the number of pixels we blit is somewhat smaller than "update_region_larger", so we have
456        *
457        * update_region < draw_region < update_region larger
458        *
459        * the extra space should compensate for the effect of fractional
460        * coordinates (which do not render at exact pixel boundaries)
461        */
462       draw_region.grow (2);
463
464       draw_region.clip (cairo_gl->width(), cairo_gl->height());
465
466       cairo_gl->draw (draw_region.x, draw_region.y, draw_region.w, draw_region.h);
467     }
468   // clear update region (will be assigned by update[_full] before next redraw)
469   update_region = Rect();
470   update_full_redraw = false;
471 }
472
473 template<class T>
474 void
475 cleanup_null (vector<T *>& vec)
476 {
477   vector<T *> new_vec;
478   for (auto object : vec)
479     if (object)
480       new_vec.push_back (object);
481
482   if (vec.size() > new_vec.size())
483     vec = new_vec;
484 }
485
486 void
487 Window::process_events()
488 {
489   if (popup_window)
490     {
491       popup_window->process_events();
492
493       if (!have_popup_window)
494         popup_window.reset(); // must be deleted after (not during) process_events */
495     }
496   if (native_file_dialog)
497     {
498       native_file_dialog->process_events();
499
500       if (!have_file_dialog)
501         {
502           /* file dialog closed - must be deleted after (not during) process_events */
503           native_file_dialog.reset();
504         }
505     }
506
507   /* do not use auto here, since timers may get modified */
508   for (size_t i = 0; i < timers.size(); i++)
509     {
510       Timer *timer = timers[i];
511       if (timer)
512         timer->process_events();
513     }
514   /* timers array may have been modified - remove null entries */
515   cleanup_null (timers);
516
517   for (size_t i = 0; i < child_windows.size(); i++)
518     {
519       Window *window = child_windows[i];
520       if (window)
521         {
522           window->process_events();
523           if (child_windows[i] == nullptr)
524             {
525               /* window closed - must be deleted after (not during) process_events */
526               delete window;
527             }
528         }
529     }
530   cleanup_null (child_windows);
531
532   if (0)
533     {
534       timeval tv;
535       gettimeofday (&tv, 0);
536
537       static double last_time = -1;
538       const double time = tv.tv_sec + tv.tv_usec / 1000000.0;
539       const double delta_time = time - last_time;
540
541       if (last_time > 0)
542         sm_debug ("process_delta_time %f %f\n", /* time diff */ delta_time, /* frames per second */ 1.0 / delta_time);
543
544       last_time = time;
545     }
546   puglProcessEvents (view);
547 }
548
549 static void
550 dump_event (const PuglEvent *event)
551 {
552   switch (event->type)
553     {
554       case PUGL_NOTHING:            printf ("Event: nothing\n");
555         break;
556       case PUGL_BUTTON_PRESS:       printf ("Event: button press\n");
557         break;
558       case PUGL_BUTTON_RELEASE:     printf ("Event: button release\n");
559         break;
560       case PUGL_CONFIGURE:          printf ("Event: configure w%f h%f\n", event->configure.width, event->configure.height);
561         break;
562       case PUGL_EXPOSE:             printf ("Event: expose x%f y%f w%f h%f\n", event->expose.x, event->expose.y, event->expose.width, event->expose.height);
563         break;
564       case PUGL_CLOSE:              printf ("Event: close\n");
565         break;
566       case PUGL_KEY_PRESS:          printf ("Event: key press %c\n", event->key.character);
567         break;
568       case PUGL_KEY_RELEASE:        printf ("Event: key release\n");
569         break;
570       case PUGL_ENTER_NOTIFY:       printf ("Event: enter\n");
571         break;
572       case PUGL_LEAVE_NOTIFY:       printf ("Event: leave\n");
573         break;
574       case PUGL_MOTION_NOTIFY:      printf ("Event: motion\n");
575         break;
576       case PUGL_SCROLL:             printf ("Event: scroll: dx=%f dy=%f\n", event->scroll.dx, event->scroll.dy);
577         break;
578       case PUGL_FOCUS_IN:           printf ("Event: focus in\n");
579         break;
580       case PUGL_FOCUS_OUT:          printf ("Event: focus out\n");
581         break;
582     }
583 }
584
585 void
586 Window::on_event (const PuglEvent* event)
587 {
588   if (0)
589     dump_event (event);
590
591   double ex, ey; /* global scale translated */
592   Widget *current_widget = nullptr;
593
594   /* as long as the file dialog or popup window is open, ignore user input */
595   const bool ignore_input = have_file_dialog || have_popup_window;
596   if (ignore_input && event->type != PUGL_EXPOSE && event->type != PUGL_CONFIGURE)
597     return;
598
599   switch (event->type)
600     {
601       bool key_handled;
602
603       case PUGL_BUTTON_PRESS:
604         ex = event->button.x / global_scale;
605         ey = event->button.y / global_scale;
606
607         mouse_widget = find_widget_xy (ex, ey);
608         mouse_widget->mouse_press (ex - mouse_widget->abs_x(), ey - mouse_widget->abs_y());
609
610         if (auto_redraw)
611           update_full();
612         break;
613       case PUGL_BUTTON_RELEASE:
614         ex = event->button.x / global_scale;
615         ey = event->button.y / global_scale;
616         if (mouse_widget)
617           {
618             Widget *w = mouse_widget;
619             w->mouse_release (ex - w->abs_x(), ey - w->abs_y());
620             mouse_widget = nullptr;
621           }
622         if (auto_redraw)
623           update_full();
624         break;
625       case PUGL_MOTION_NOTIFY:
626         ex = event->motion.x / global_scale;
627         ey = event->motion.y / global_scale;
628         if (mouse_widget) /* user interaction with one widget */
629           {
630             current_widget = mouse_widget;
631           }
632         else
633           {
634             current_widget = find_widget_xy (ex, ey);
635             if (enter_widget != current_widget)
636               {
637                 if (enter_widget)
638                   enter_widget->leave_event();
639
640                 enter_widget = current_widget;
641                 current_widget->enter_event();
642               }
643           }
644         current_widget->motion (ex - current_widget->abs_x(), ey - current_widget->abs_y());
645         if (auto_redraw)
646           update_full();
647         break;
648       case PUGL_SCROLL:
649         ex = event->scroll.x / global_scale;
650         ey = event->scroll.y / global_scale;
651         if (mouse_widget)
652           current_widget = mouse_widget;
653         else
654           current_widget = find_widget_xy (ex, ey);
655
656         while (current_widget)
657           {
658             if (current_widget->scroll (event->scroll.dx, event->scroll.dy))
659               break;
660
661             current_widget = current_widget->parent;
662           }
663         if (auto_redraw)
664           update_full();
665         break;
666       case PUGL_KEY_PRESS:
667         key_handled = false;
668         /* do not use auto here, since shortcuts may get modified */
669         cleanup_null (shortcuts);
670         for (size_t i = 0; i < shortcuts.size(); i++)
671           {
672             Shortcut *shortcut = shortcuts[i];
673             if (!key_handled && shortcut)
674               key_handled = shortcut->key_press_event (event->key);
675           }
676         if (!key_handled && keyboard_focus_widget)
677           keyboard_focus_widget->key_press_event (event->key);
678         else if (!key_handled)
679           {
680             if (event->key.character == 'g')
681               draw_grid = !draw_grid;
682             else if (event->key.character == 'u')
683               debug_update_region = !debug_update_region;
684           }
685         if (auto_redraw)
686           update_full();
687         break;
688       case PUGL_CLOSE:
689         if (m_close_callback)
690           m_close_callback();
691         break;
692       case PUGL_EXPOSE:
693         on_display();
694         break;
695       case PUGL_CONFIGURE:
696         {
697           int w, h;
698           get_scaled_size (&w, &h);
699           cairo_gl.reset (new CairoGL (w, h));
700
701           // on windows, the coordinates of the event often doesn't match actual size
702           // cairo_gl.reset (new CairoGL (event->configure.width, event->configure.height));
703         }
704         puglEnterContext (view);
705         cairo_gl->configure();
706         puglLeaveContext (view, false);
707         update_full();
708         break;
709       default:
710         break;
711     }
712 }
713
714 Widget *
715 Window::find_widget_xy (double ex, double ey)
716 {
717   Widget *widget = this;
718
719   if (menu_widget)
720     widget = menu_widget;  // active menu => only children of the menu get clicks
721
722   if (dialog_widget)
723     widget = dialog_widget;
724
725   for (auto w : ::crawl_widgets ({ widget })) // which child gets the click?
726     {
727       if (get_visible_recursive (w) && w->recursive_enabled() && w->abs_visible_rect().contains (ex, ey))
728         {
729           widget = w;
730         }
731     }
732   return widget;
733 }
734
735 void
736 Window::show()
737 {
738   puglPostRedisplay (view);
739   puglShowWindow (view);
740 }
741
742 void
743 Window::open_file_dialog (const string& title, const string& filter_title, const string& filter, std::function<void(string)> callback)
744 {
745   PuglNativeWindow win_id = puglGetNativeWindow (view);
746
747   file_dialog_callback = callback;
748   have_file_dialog = true;
749
750   native_file_dialog.reset (NativeFileDialog::create (win_id, true, title, filter_title, filter));
751   connect (native_file_dialog->signal_file_selected, this, &Window::on_file_selected);
752 }
753
754 void
755 Window::save_file_dialog (const string& title, const string& filter_title, const string& filter, std::function<void(string)> callback)
756 {
757   PuglNativeWindow win_id = puglGetNativeWindow (view);
758
759   file_dialog_callback = callback;
760   have_file_dialog = true;
761
762   native_file_dialog.reset (NativeFileDialog::create (win_id, false, title, filter_title, filter));
763   connect (native_file_dialog->signal_file_selected, this, &Window::on_file_selected);
764 }
765
766 void
767 Window::on_file_selected (const std::string& filename)
768 {
769   if (file_dialog_callback)
770     {
771       file_dialog_callback (filename);
772       file_dialog_callback = nullptr;
773     }
774   have_file_dialog = false;
775   update_full();
776 }
777
778 void
779 Window::need_update (Widget *widget, const Rect *changed_rect)
780 {
781   if (widget)
782     {
783       Rect widget_rect = widget->abs_visible_rect();
784       if (changed_rect)
785         {
786           /* if changed rect is set, we only need to redraw a part of the widget */
787           Rect abs_changed_rect;
788           abs_changed_rect = Rect (changed_rect->x() + widget->abs_x(),
789                                    changed_rect->y() + widget->abs_y(),
790                                    changed_rect->width(),
791                                    changed_rect->height());
792           widget_rect = widget_rect.intersection (abs_changed_rect);
793         }
794       update_region = update_region.rect_union (widget_rect);
795     }
796   else
797     update_full_redraw = true;
798
799   puglPostRedisplay (view);
800 }
801
802 Window *
803 Window::window()
804 {
805   return this;
806 }
807
808 void
809 Window::wait_event_fps()
810 {
811   /* tradeoff between UI responsiveness and cpu usage caused by thread wakeups
812    *
813    * 60 fps should make the UI look smooth
814    */
815   const double frames_per_second = 60;
816
817   usleep (1000 * 1000 / frames_per_second);
818 }
819
820 void
821 Window::wait_for_event()
822 {
823   bool active_timer = false;
824   for (auto t : timers)
825     {
826       if (t)
827         active_timer = t->active() || active_timer;
828     }
829   if (native_file_dialog || popup_window || active_timer)
830     {
831       /* need to wait for events of this view, and handle io for file dialog */
832       wait_event_fps();
833     }
834   else
835     {
836       puglWaitForEvent (view);
837     }
838 }
839
840 void
841 Window::set_menu_widget (Widget *widget)
842 {
843   menu_widget = widget;
844   mouse_widget = widget;
845 }
846
847 void
848 Window::set_close_callback (const std::function<void()>& callback)
849 {
850   m_close_callback = callback;
851 }
852
853 void
854 Window::set_keyboard_focus (Widget *widget)
855 {
856   keyboard_focus_widget = widget;
857 }
858
859 void
860 Window::set_dialog_widget (Widget *widget)
861 {
862   dialog_widget = widget;
863 }
864
865 void
866 Window::set_popup_window (Window *pwin)
867 {
868   if (pwin)
869     {
870       // take ownership
871       popup_window.reset (pwin);
872
873       have_popup_window = true;
874     }
875   else
876     {
877       have_popup_window = false;
878     }
879   update_full();
880 }
881
882 void
883 Window::add_child_window (Window *cwin)
884 {
885   child_windows.push_back (cwin);
886 }
887
888 void
889 Window::remove_child_window (Window *cwin)
890 {
891   for (auto& c : child_windows)
892     {
893       if (c == cwin)
894         c = nullptr;
895     }
896 }
897
898 PuglNativeWindow
899 Window::native_window()
900 {
901   return puglGetNativeWindow (view);
902 }
903
904 void
905 Window::fill_zoom_menu (Menu *menu)
906 {
907   menu->clear();
908
909   for (int z = 70; z <= 500; )
910     {
911       int w = width * z / 100;
912       int h = height * z / 100;
913
914       string text = string_locale_printf ("%d%%   -   %dx%d", z, w, h);
915
916       if (sm_round_positive (window()->gui_scaling() * 100) == z)
917         text += "   -   current zoom";
918       MenuItem *item = menu->add_item (text);
919       connect (item->signal_clicked, [=]() {
920         window()->set_gui_scaling (z / 100.);
921
922         // we need to refill the menu to update the "current zoom" entry
923         fill_zoom_menu (menu);
924       });
925
926       if (z >= 400)
927         z += 50;
928       else if (z >= 300)
929         z += 25;
930       else if (z >= 200)
931         z += 20;
932       else
933         z += 10;
934     }
935 }
936
937 void
938 Window::set_gui_scaling (double s)
939 {
940   global_scale = s;
941
942   /* restart with this gui scaling next time */
943   Config cfg;
944
945   cfg.set_zoom (sm_round_positive (s * 100));
946   cfg.store();
947
948   /* (1) typically, at this point we notify the host that our window will have a new size */
949   signal_update_size();
950
951   /* (2) and we ensure that our window size will be changed via pugl */
952   puglPostResize (view);
953 }
954
955 double
956 Window::gui_scaling()
957 {
958   return global_scale;
959 }
960
961 void
962 Window::on_resize (int *win_width, int *win_height)
963 {
964   get_scaled_size (win_width, win_height);
965 }
966
967 void
968 Window::get_scaled_size (int *w, int *h)
969 {
970   *w = width * global_scale;
971   *h = height * global_scale;
972 }
973
974 void
975 Window::add_timer (Timer *timer)
976 {
977   timers.push_back (timer);
978 }
979
980 void
981 Window::remove_timer (Timer *timer)
982 {
983   for (auto& t : timers)
984     {
985       if (t == timer)
986         t = nullptr;
987     }
988 }
989
990 void
991 Window::add_shortcut (Shortcut *shortcut)
992 {
993   shortcuts.push_back (shortcut);
994 }
995
996 void
997 Window::remove_shortcut (Shortcut *shortcut)
998 {
999   for (auto& s : shortcuts)
1000     {
1001       if (s == shortcut)
1002         s = nullptr;
1003     }
1004 }