4f99b1a165fd3dbd426c59b064773d398d9b3933
[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         if (keyboard_focus_widget && keyboard_focus_release_on_click)
609           {
610             keyboard_focus_widget->focus_out_event();
611             keyboard_focus_widget = nullptr;
612           }
613         else
614           {
615             mouse_widget->mouse_press (ex - mouse_widget->abs_x(), ey - mouse_widget->abs_y());
616           }
617
618         if (auto_redraw)
619           update_full();
620         break;
621       case PUGL_BUTTON_RELEASE:
622         ex = event->button.x / global_scale;
623         ey = event->button.y / global_scale;
624         if (mouse_widget)
625           {
626             Widget *w = mouse_widget;
627             w->mouse_release (ex - w->abs_x(), ey - w->abs_y());
628             mouse_widget = nullptr;
629           }
630         if (auto_redraw)
631           update_full();
632         break;
633       case PUGL_MOTION_NOTIFY:
634         ex = event->motion.x / global_scale;
635         ey = event->motion.y / global_scale;
636         if (mouse_widget) /* user interaction with one widget */
637           {
638             current_widget = mouse_widget;
639           }
640         else
641           {
642             current_widget = find_widget_xy (ex, ey);
643             if (enter_widget != current_widget)
644               {
645                 if (enter_widget)
646                   enter_widget->leave_event();
647
648                 enter_widget = current_widget;
649                 current_widget->enter_event();
650               }
651           }
652         current_widget->motion (ex - current_widget->abs_x(), ey - current_widget->abs_y());
653         if (auto_redraw)
654           update_full();
655         break;
656       case PUGL_SCROLL:
657         ex = event->scroll.x / global_scale;
658         ey = event->scroll.y / global_scale;
659         if (mouse_widget)
660           current_widget = mouse_widget;
661         else
662           current_widget = find_widget_xy (ex, ey);
663
664         while (current_widget)
665           {
666             if (current_widget->scroll (event->scroll.dx, event->scroll.dy))
667               break;
668
669             current_widget = current_widget->parent;
670           }
671         if (auto_redraw)
672           update_full();
673         break;
674       case PUGL_KEY_PRESS:
675         key_handled = false;
676         /* do not use auto here, since shortcuts may get modified */
677         cleanup_null (shortcuts);
678         for (size_t i = 0; i < shortcuts.size(); i++)
679           {
680             Shortcut *shortcut = shortcuts[i];
681             if (!key_handled && shortcut)
682               key_handled = shortcut->key_press_event (event->key);
683           }
684         if (!key_handled && keyboard_focus_widget)
685           keyboard_focus_widget->key_press_event (event->key);
686         else if (!key_handled)
687           {
688             if (event->key.character == 'g')
689               draw_grid = !draw_grid;
690             else if (event->key.character == 'u')
691               debug_update_region = !debug_update_region;
692           }
693         if (auto_redraw)
694           update_full();
695         break;
696       case PUGL_CLOSE:
697         if (m_close_callback)
698           m_close_callback();
699         break;
700       case PUGL_EXPOSE:
701         on_display();
702         break;
703       case PUGL_CONFIGURE:
704         {
705           int w, h;
706           get_scaled_size (&w, &h);
707           cairo_gl.reset (new CairoGL (w, h));
708
709           // on windows, the coordinates of the event often doesn't match actual size
710           // cairo_gl.reset (new CairoGL (event->configure.width, event->configure.height));
711         }
712         puglEnterContext (view);
713         cairo_gl->configure();
714         puglLeaveContext (view, false);
715         update_full();
716         break;
717       default:
718         break;
719     }
720 }
721
722 Widget *
723 Window::find_widget_xy (double ex, double ey)
724 {
725   Widget *widget = this;
726
727   if (menu_widget)
728     widget = menu_widget;  // active menu => only children of the menu get clicks
729
730   if (dialog_widget)
731     widget = dialog_widget;
732
733   for (auto w : ::crawl_widgets ({ widget })) // which child gets the click?
734     {
735       if (get_visible_recursive (w) && w->recursive_enabled() && w->abs_visible_rect().contains (ex, ey))
736         {
737           widget = w;
738         }
739     }
740   return widget;
741 }
742
743 void
744 Window::show()
745 {
746   puglPostRedisplay (view);
747   puglShowWindow (view);
748 }
749
750 void
751 Window::open_file_dialog (const string& title, const string& filter_title, const string& filter, std::function<void(string)> callback)
752 {
753   PuglNativeWindow win_id = puglGetNativeWindow (view);
754
755   file_dialog_callback = callback;
756   have_file_dialog = true;
757
758   native_file_dialog.reset (NativeFileDialog::create (win_id, true, title, filter_title, filter));
759   connect (native_file_dialog->signal_file_selected, this, &Window::on_file_selected);
760 }
761
762 void
763 Window::save_file_dialog (const string& title, const string& filter_title, const string& filter, std::function<void(string)> callback)
764 {
765   PuglNativeWindow win_id = puglGetNativeWindow (view);
766
767   file_dialog_callback = callback;
768   have_file_dialog = true;
769
770   native_file_dialog.reset (NativeFileDialog::create (win_id, false, title, filter_title, filter));
771   connect (native_file_dialog->signal_file_selected, this, &Window::on_file_selected);
772 }
773
774 void
775 Window::on_file_selected (const std::string& filename)
776 {
777   if (file_dialog_callback)
778     {
779       file_dialog_callback (filename);
780       file_dialog_callback = nullptr;
781     }
782   have_file_dialog = false;
783   update_full();
784 }
785
786 void
787 Window::need_update (Widget *widget, const Rect *changed_rect)
788 {
789   if (widget)
790     {
791       Rect widget_rect = widget->abs_visible_rect();
792       if (changed_rect)
793         {
794           /* if changed rect is set, we only need to redraw a part of the widget */
795           Rect abs_changed_rect;
796           abs_changed_rect = Rect (changed_rect->x() + widget->abs_x(),
797                                    changed_rect->y() + widget->abs_y(),
798                                    changed_rect->width(),
799                                    changed_rect->height());
800           widget_rect = widget_rect.intersection (abs_changed_rect);
801         }
802       update_region = update_region.rect_union (widget_rect);
803     }
804   else
805     update_full_redraw = true;
806
807   puglPostRedisplay (view);
808 }
809
810 Window *
811 Window::window()
812 {
813   return this;
814 }
815
816 void
817 Window::wait_event_fps()
818 {
819   /* tradeoff between UI responsiveness and cpu usage caused by thread wakeups
820    *
821    * 60 fps should make the UI look smooth
822    */
823   const double frames_per_second = 60;
824
825   usleep (1000 * 1000 / frames_per_second);
826 }
827
828 void
829 Window::wait_for_event()
830 {
831   bool active_timer = false;
832   for (auto t : timers)
833     {
834       if (t)
835         active_timer = t->active() || active_timer;
836     }
837   if (native_file_dialog || popup_window || active_timer)
838     {
839       /* need to wait for events of this view, and handle io for file dialog */
840       wait_event_fps();
841     }
842   else
843     {
844       puglWaitForEvent (view);
845     }
846 }
847
848 void
849 Window::set_menu_widget (Widget *widget)
850 {
851   menu_widget = widget;
852   mouse_widget = widget;
853 }
854
855 void
856 Window::set_close_callback (const std::function<void()>& callback)
857 {
858   m_close_callback = callback;
859 }
860
861 void
862 Window::set_keyboard_focus (Widget *widget, bool release_on_click)
863 {
864   keyboard_focus_widget = widget;
865   keyboard_focus_widget->focus_event();
866   keyboard_focus_release_on_click = release_on_click;
867 }
868
869 void
870 Window::set_dialog_widget (Widget *widget)
871 {
872   dialog_widget = widget;
873 }
874
875 void
876 Window::set_popup_window (Window *pwin)
877 {
878   if (pwin)
879     {
880       // take ownership
881       popup_window.reset (pwin);
882
883       have_popup_window = true;
884     }
885   else
886     {
887       have_popup_window = false;
888     }
889   update_full();
890 }
891
892 void
893 Window::add_child_window (Window *cwin)
894 {
895   child_windows.push_back (cwin);
896 }
897
898 void
899 Window::remove_child_window (Window *cwin)
900 {
901   for (auto& c : child_windows)
902     {
903       if (c == cwin)
904         c = nullptr;
905     }
906 }
907
908 PuglNativeWindow
909 Window::native_window()
910 {
911   return puglGetNativeWindow (view);
912 }
913
914 void
915 Window::fill_zoom_menu (Menu *menu)
916 {
917   menu->clear();
918
919   for (int z = 70; z <= 500; )
920     {
921       int w = width * z / 100;
922       int h = height * z / 100;
923
924       string text = string_locale_printf ("%d%%   -   %dx%d", z, w, h);
925
926       if (sm_round_positive (window()->gui_scaling() * 100) == z)
927         text += "   -   current zoom";
928       MenuItem *item = menu->add_item (text);
929       connect (item->signal_clicked, [=]() {
930         window()->set_gui_scaling (z / 100.);
931
932         // we need to refill the menu to update the "current zoom" entry
933         fill_zoom_menu (menu);
934       });
935
936       if (z >= 400)
937         z += 50;
938       else if (z >= 300)
939         z += 25;
940       else if (z >= 200)
941         z += 20;
942       else
943         z += 10;
944     }
945 }
946
947 void
948 Window::set_gui_scaling (double s)
949 {
950   global_scale = s;
951
952   /* restart with this gui scaling next time */
953   Config cfg;
954
955   cfg.set_zoom (sm_round_positive (s * 100));
956   cfg.store();
957
958   /* (1) typically, at this point we notify the host that our window will have a new size */
959   signal_update_size();
960
961   /* (2) and we ensure that our window size will be changed via pugl */
962   puglPostResize (view);
963 }
964
965 double
966 Window::gui_scaling()
967 {
968   return global_scale;
969 }
970
971 void
972 Window::on_resize (int *win_width, int *win_height)
973 {
974   get_scaled_size (win_width, win_height);
975 }
976
977 void
978 Window::get_scaled_size (int *w, int *h)
979 {
980   *w = width * global_scale;
981   *h = height * global_scale;
982 }
983
984 void
985 Window::add_timer (Timer *timer)
986 {
987   timers.push_back (timer);
988 }
989
990 void
991 Window::remove_timer (Timer *timer)
992 {
993   for (auto& t : timers)
994     {
995       if (t == timer)
996         t = nullptr;
997     }
998 }
999
1000 void
1001 Window::add_shortcut (Shortcut *shortcut)
1002 {
1003   shortcuts.push_back (shortcut);
1004 }
1005
1006 void
1007 Window::remove_shortcut (Shortcut *shortcut)
1008 {
1009   for (auto& s : shortcuts)
1010     {
1011       if (s == shortcut)
1012         s = nullptr;
1013     }
1014 }