1 /*
2   Copyright 2012 David Robillard <http://drobilla.net>
3 
4   Permission to use, copy, modify, and/or distribute this software for any
5   purpose with or without fee is hereby granted, provided that the above
6   copyright notice and this permission notice appear in all copies.
7 
8   THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9   WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10   MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11   ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12   WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13   ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14   OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15 */
16 
17 /**
18    @file pugl_win.cpp Windows/WGL Pugl Implementation.
19 */
20 
21 #include <windows.h>
22 #include <windowsx.h>
23 #include <GL/gl.h>
24 
25 #include <stdio.h>
26 #include <stdlib.h>
27 
28 #include "pugl_internal.h"
29 
30 #ifndef WM_MOUSEWHEEL
31 #    define WM_MOUSEWHEEL 0x020A
32 #endif
33 #ifndef WM_MOUSEHWHEEL
34 #    define WM_MOUSEHWHEEL 0x020E
35 #endif
36 #ifndef WHEEL_DELTA
37 #    define WHEEL_DELTA 120
38 #endif
39 #ifndef GWL_USERDATA
40 #    define GWL_USERDATA (-21)
41 #endif
42 
43 const int LOCAL_CLOSE_MSG = WM_USER + 50;
44 
45 struct PuglInternalsImpl {
46 	HWND     hwnd;
47 	HDC      hdc;
48 	HGLRC    hglrc;
49 	WNDCLASS wc;
50 	bool     keep_aspect;
51 	int      win_flags;
52 };
53 
54 LRESULT CALLBACK
55 wndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam);
56 
57 PuglView*
puglCreate(PuglNativeWindow parent,const char * title,int min_width,int min_height,int width,int height,bool resizable,bool ontop,unsigned long transientId)58 puglCreate(PuglNativeWindow parent,
59            const char*      title,
60            int              min_width,
61            int              min_height,
62            int              width,
63            int              height,
64            bool             resizable,
65            bool             ontop,
66            unsigned long    transientId)
67 {
68 	PuglView*      view = (PuglView*)calloc(1, sizeof(PuglView));
69 	PuglInternals* impl = (PuglInternals*)calloc(1, sizeof(PuglInternals));
70 	if (!view || !impl) {
71 		return NULL;
72 	}
73 
74 	view->impl   = impl;
75 	view->width  = width;
76 	view->height = height;
77 	view->ontop  = ontop;
78 	view->user_resizable = resizable && !parent;
79 	view->impl->keep_aspect = min_width != width;
80 
81 	// FIXME: This is nasty, and pugl should not have static anything.
82 	// Should class be a parameter?  Does this make sense on other platforms?
83 	static int wc_count = 0;
84 	int retry = 99;
85 	char classNameBuf[256];
86 	while (true) {
87 		_snprintf(classNameBuf, sizeof(classNameBuf), "x%d%s", wc_count++, title);
88 		classNameBuf[sizeof(classNameBuf)-1] = '\0';
89 
90 		impl->wc.style         = CS_OWNDC;
91 		impl->wc.lpfnWndProc   = wndProc;
92 		impl->wc.cbClsExtra    = 0;
93 		impl->wc.cbWndExtra    = 0;
94 		impl->wc.hInstance     = 0;
95 		impl->wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
96 		impl->wc.hCursor       = LoadCursor(NULL, IDC_ARROW);
97 		impl->wc.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH);
98 		impl->wc.lpszMenuName  = NULL;
99 		impl->wc.lpszClassName = strdup(classNameBuf);
100 
101 		if (RegisterClass(&impl->wc)) {
102 			break;
103 		}
104 		if (--retry > 0) {
105 			free((void*)impl->wc.lpszClassName);
106 			continue;
107 		}
108 		/* fail */
109 		free((void*)impl->wc.lpszClassName);
110 		free(impl);
111 		free(view);
112 		return NULL;
113 	}
114 
115 	if (parent) {
116 		view->impl->win_flags = WS_CHILD;
117 	} else {
118 		view->impl->win_flags = WS_POPUPWINDOW | WS_CAPTION | (view->user_resizable ? WS_SIZEBOX : 0);
119 	}
120 
121 	// Adjust the overall window size to accomodate our requested client size
122 	RECT wr = { 0, 0, width, height };
123 	AdjustWindowRectEx(&wr, view->impl->win_flags, FALSE, WS_EX_TOPMOST);
124 
125 	RECT mr = { 0, 0, min_width, min_height };
126 	AdjustWindowRectEx(&mr, view->impl->win_flags, FALSE, WS_EX_TOPMOST);
127 	view->min_width  = mr.right - mr.left;
128 	view->min_height = mr.bottom - mr.top;
129 
130 	impl->hwnd = CreateWindowEx (
131 		WS_EX_TOPMOST,
132 		classNameBuf, title, (view->user_resizable ? WS_SIZEBOX : 0) |
133 		(parent ? (WS_CHILD | WS_VISIBLE) : (WS_POPUPWINDOW | WS_CAPTION)),
134 		0, 0, wr.right - wr.left, wr.bottom - wr.top,
135 		(HWND)parent, NULL, NULL, NULL);
136 
137 	if (!impl->hwnd) {
138 		free((void*)impl->wc.lpszClassName);
139 		free(impl);
140 		free(view);
141 		return NULL;
142 	}
143 
144 	SetWindowLongPtr(impl->hwnd, GWL_USERDATA, (LONG_PTR)view);
145 
146 	SetWindowPos (impl->hwnd,
147 			ontop ? HWND_TOPMOST : HWND_TOP,
148 			0, 0, 0, 0, (ontop ? 0 : SWP_NOACTIVATE) | SWP_ASYNCWINDOWPOS | SWP_NOMOVE | SWP_NOSIZE);
149 
150 	impl->hdc = GetDC(impl->hwnd);
151 
152 	PIXELFORMATDESCRIPTOR pfd;
153 	ZeroMemory(&pfd, sizeof(pfd));
154 	pfd.nSize      = sizeof(pfd);
155 	pfd.nVersion   = 1;
156 	pfd.dwFlags    = PFD_DRAW_TO_WINDOW | PFD_SUPPORT_OPENGL | PFD_DOUBLEBUFFER;
157 	pfd.iPixelType = PFD_TYPE_RGBA;
158 	pfd.cColorBits = 24;
159 	pfd.cDepthBits = 16;
160 	pfd.iLayerType = PFD_MAIN_PLANE;
161 
162 	int format = ChoosePixelFormat(impl->hdc, &pfd);
163 	SetPixelFormat(impl->hdc, format, &pfd);
164 
165 	impl->hglrc = wglCreateContext(impl->hdc);
166 	if (!impl->hglrc) {
167 		ReleaseDC (impl->hwnd, impl->hdc);
168 		DestroyWindow (impl->hwnd);
169 		UnregisterClass (impl->wc.lpszClassName, NULL);
170 		free((void*)impl->wc.lpszClassName);
171 		free(impl);
172 		free(view);
173 		return NULL;
174 	}
175 	wglMakeCurrent(impl->hdc, impl->hglrc);
176 
177 	view->width  = width;
178 	view->height = height;
179 
180 	return view;
181 }
182 
183 void
puglDestroy(PuglView * view)184 puglDestroy(PuglView* view)
185 {
186 	if (!view) {
187 		return;
188 	}
189 	wglMakeCurrent(NULL, NULL);
190 	wglDeleteContext(view->impl->hglrc);
191 	ReleaseDC(view->impl->hwnd, view->impl->hdc);
192 	DestroyWindow(view->impl->hwnd);
193 	UnregisterClass(view->impl->wc.lpszClassName, NULL);
194 	free((void*)view->impl->wc.lpszClassName);
195 	free(view->impl);
196 	free(view);
197 }
198 
199 PUGL_API void
puglShowWindow(PuglView * view)200 puglShowWindow(PuglView* view) {
201 	ShowWindow(view->impl->hwnd, WS_VISIBLE);
202 	ShowWindow(view->impl->hwnd, SW_RESTORE);
203 	//SetForegroundWindow(view->impl->hwnd);
204 	UpdateWindow(view->impl->hwnd);
205 }
206 
207 PUGL_API void
puglHideWindow(PuglView * view)208 puglHideWindow(PuglView* view) {
209 	ShowWindow(view->impl->hwnd, SW_HIDE);
210 	UpdateWindow(view->impl->hwnd);
211 }
212 
213 static void
puglReshape(PuglView * view,int width,int height)214 puglReshape(PuglView* view, int width, int height)
215 {
216 	wglMakeCurrent(view->impl->hdc, view->impl->hglrc);
217 
218 	if (view->reshapeFunc) {
219 		view->reshapeFunc(view, width, height);
220 	} else {
221 		puglDefaultReshape(view, width, height);
222 	}
223 
224 	wglMakeCurrent(NULL, NULL);
225 
226 	view->width  = width;
227 	view->height = height;
228 }
229 
230 static void
puglDisplay(PuglView * view)231 puglDisplay(PuglView* view)
232 {
233 	wglMakeCurrent(view->impl->hdc, view->impl->hglrc);
234 #if 0
235 	glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
236 	glLoadIdentity();
237 #endif
238 
239 	view->redisplay = false;
240 	if (view->displayFunc) {
241 		view->displayFunc(view);
242 	}
243 
244 	glFlush();
245 	SwapBuffers(view->impl->hdc);
246 	wglMakeCurrent(NULL, NULL);
247 }
248 
249 static void
puglResize(PuglView * view)250 puglResize(PuglView* view)
251 {
252 	int set_hints; // ignored for now
253 	view->resize = false;
254 	if (!view->resizeFunc) { return; }
255 	/* ask the plugin about the new size */
256 	view->resizeFunc(view, &view->width, &view->height, &set_hints);
257 
258 	HWND parent = GetParent (view->impl->hwnd);
259 	if (parent) {
260 		puglReshape(view, view->width, view->height);
261 		SetWindowPos (view->impl->hwnd, HWND_TOP,
262 				0, 0, view->width, view->height,
263 				SWP_NOZORDER | SWP_NOMOVE);
264 		return;
265 	}
266 
267 	RECT wr = { 0, 0, (long)view->width, (long)view->height };
268 	AdjustWindowRectEx(&wr, view->impl->win_flags, FALSE, WS_EX_TOPMOST);
269 	SetWindowPos (view->impl->hwnd,
270 			view->ontop ? HWND_TOPMOST : HWND_NOTOPMOST,
271 			0, 0, wr.right-wr.left, wr.bottom-wr.top,
272 			SWP_NOACTIVATE|SWP_NOMOVE|SWP_NOOWNERZORDER /*|SWP_NOZORDER*/);
273 	UpdateWindow(view->impl->hwnd);
274 
275 	/* and call Reshape in GL context */
276 	puglReshape(view, view->width, view->height);
277 }
278 
279 static PuglKey
keySymToSpecial(int sym)280 keySymToSpecial(int sym)
281 {
282 	switch (sym) {
283 	case VK_F1:      return PUGL_KEY_F1;
284 	case VK_F2:      return PUGL_KEY_F2;
285 	case VK_F3:      return PUGL_KEY_F3;
286 	case VK_F4:      return PUGL_KEY_F4;
287 	case VK_F5:      return PUGL_KEY_F5;
288 	case VK_F6:      return PUGL_KEY_F6;
289 	case VK_F7:      return PUGL_KEY_F7;
290 	case VK_F8:      return PUGL_KEY_F8;
291 	case VK_F9:      return PUGL_KEY_F9;
292 	case VK_F10:     return PUGL_KEY_F10;
293 	case VK_F11:     return PUGL_KEY_F11;
294 	case VK_F12:     return PUGL_KEY_F12;
295 	case VK_LEFT:    return PUGL_KEY_LEFT;
296 	case VK_UP:      return PUGL_KEY_UP;
297 	case VK_RIGHT:   return PUGL_KEY_RIGHT;
298 	case VK_DOWN:    return PUGL_KEY_DOWN;
299 	case VK_PRIOR:   return PUGL_KEY_PAGE_UP;
300 	case VK_NEXT:    return PUGL_KEY_PAGE_DOWN;
301 	case VK_HOME:    return PUGL_KEY_HOME;
302 	case VK_END:     return PUGL_KEY_END;
303 	case VK_INSERT:  return PUGL_KEY_INSERT;
304 	case VK_SHIFT:   return PUGL_KEY_SHIFT;
305 	case VK_CONTROL: return PUGL_KEY_CTRL;
306 	case VK_MENU:    return PUGL_KEY_ALT;
307 	case VK_LWIN:    return PUGL_KEY_SUPER;
308 	case VK_RWIN:    return PUGL_KEY_SUPER;
309 	}
310 	return (PuglKey)0;
311 }
312 
313 static void
processMouseEvent(PuglView * view,int button,bool press,LPARAM lParam)314 processMouseEvent(PuglView* view, int button, bool press, LPARAM lParam)
315 {
316 	view->event_timestamp_ms = GetMessageTime();
317 	if (GetFocus() != view->impl->hwnd) {
318 		// focus is needed to receive mouse-wheel events
319 		SetFocus (view->impl->hwnd);
320 	}
321 
322 	if (press) {
323 		SetCapture(view->impl->hwnd);
324 	} else {
325 		ReleaseCapture();
326 	}
327 
328 	if (view->mouseFunc) {
329 		view->mouseFunc(view, button, press,
330 		                GET_X_LPARAM(lParam),
331 		                GET_Y_LPARAM(lParam));
332 	}
333 }
334 
335 static void
setModifiers(PuglView * view)336 setModifiers(PuglView* view)
337 {
338 	view->mods = 0;
339 	view->mods |= (GetKeyState(VK_SHIFT)   < 0) ? PUGL_MOD_SHIFT  : 0;
340 	view->mods |= (GetKeyState(VK_CONTROL) < 0) ? PUGL_MOD_CTRL   : 0;
341 	view->mods |= (GetKeyState(VK_MENU)    < 0) ? PUGL_MOD_ALT    : 0;
342 	view->mods |= (GetKeyState(VK_LWIN)    < 0) ? PUGL_MOD_SUPER  : 0;
343 	view->mods |= (GetKeyState(VK_RWIN)    < 0) ? PUGL_MOD_SUPER  : 0;
344 }
345 
346 static LRESULT
handleMessage(PuglView * view,UINT message,WPARAM wParam,LPARAM lParam)347 handleMessage(PuglView* view, UINT message, WPARAM wParam, LPARAM lParam)
348 {
349 	PAINTSTRUCT ps;
350 	PuglKey     key;
351 
352 	setModifiers(view);
353 	switch (message) {
354 	case WM_CREATE:
355 	case WM_SHOWWINDOW:
356 	case WM_SIZE:
357 		{
358 			RECT rect;
359 			GetClientRect(view->impl->hwnd, &rect);
360 			puglReshape(view, rect.right, rect.bottom);
361 			view->width = rect.right;
362 			view->height = rect.bottom;
363 		}
364 		break;
365 	case WM_SIZING:
366 		if (view->impl->keep_aspect) {
367 			float aspect = view->min_width / (float)view->min_height;
368 			RECT* rect = (RECT*)lParam;
369 			switch ((int)wParam) {
370 				case WMSZ_LEFT:
371 				case WMSZ_RIGHT:
372 				case WMSZ_BOTTOMLEFT:
373 				case WMSZ_BOTTOMRIGHT:
374 					rect->bottom = rect->top + (rect->right - rect->left) / aspect;
375 					break;
376 				case WMSZ_TOP:
377 				case WMSZ_BOTTOM:
378 				case WMSZ_TOPRIGHT:
379 					rect->right = rect->left + (rect->bottom - rect->top) * aspect;
380 					break;
381 				case WMSZ_TOPLEFT:
382 					rect->left = rect->right - (rect->bottom - rect->top) * aspect;
383 					break;
384 			}
385 			return TRUE;
386 		}
387 		break;
388 	case WM_GETMINMAXINFO:
389 		{
390 		MINMAXINFO *mmi = (MINMAXINFO*)lParam;
391 		mmi->ptMinTrackSize.x = view->min_width;
392 		mmi->ptMinTrackSize.y = view->min_height;
393 		}
394 		break;
395 	case WM_PAINT:
396 		BeginPaint(view->impl->hwnd, &ps);
397 		puglDisplay(view);
398 		EndPaint(view->impl->hwnd, &ps);
399 		break;
400 	case WM_MOUSEMOVE:
401 		if (view->motionFunc) {
402 			view->motionFunc(view, GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam));
403 		}
404 		break;
405 	case WM_LBUTTONDOWN:
406 		processMouseEvent(view, 1, true, lParam);
407 		break;
408 	case WM_MBUTTONDOWN:
409 		processMouseEvent(view, 2, true, lParam);
410 		break;
411 	case WM_RBUTTONDOWN:
412 		processMouseEvent(view, 3, true, lParam);
413 		break;
414 	case WM_LBUTTONUP:
415 		processMouseEvent(view, 1, false, lParam);
416 		break;
417 	case WM_MBUTTONUP:
418 		processMouseEvent(view, 2, false, lParam);
419 		break;
420 	case WM_RBUTTONUP:
421 		processMouseEvent(view, 3, false, lParam);
422 		break;
423 	case WM_MOUSEWHEEL:
424 		if (view->scrollFunc) {
425 			view->event_timestamp_ms = GetMessageTime();
426 			POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
427 			ScreenToClient(view->impl->hwnd, &pt);
428 			view->scrollFunc(
429 				view, pt.x, pt.y,
430 				0, GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA);
431 		}
432 		break;
433 	case WM_MOUSEHWHEEL:
434 		if (view->scrollFunc) {
435 			view->event_timestamp_ms = GetMessageTime();
436 			POINT pt = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
437 			ScreenToClient(view->impl->hwnd, &pt);
438 			view->scrollFunc(
439 				view, pt.x, pt.y,
440 				GET_WHEEL_DELTA_WPARAM(wParam) / (float)WHEEL_DELTA, 0);
441 		}
442 		break;
443 	case WM_KEYDOWN:
444 		if (view->ignoreKeyRepeat && (lParam & (1 << 30))) {
445 			break;
446 		} // else nobreak
447 	case WM_KEYUP:
448 		view->event_timestamp_ms = GetMessageTime();
449 		if ((key = keySymToSpecial(wParam))) {
450 			if (view->specialFunc) {
451 				view->specialFunc(view, message == WM_KEYDOWN, key);
452 			}
453 		} else if (view->keyboardFunc) {
454 			static BYTE kbs[256];
455 			if (GetKeyboardState(kbs) != FALSE) {
456 				char lb[2];
457 				UINT scanCode = (lParam >> 8) & 0xFFFFFF00;
458 				if ( 1 == ToAscii(wParam, scanCode, kbs, (LPWORD)lb, 0)) {
459 					view->keyboardFunc(view, message == WM_KEYDOWN, (char)lb[0]);
460 				}
461 			}
462 		}
463 		break;
464 	case WM_SETFOCUS:
465 		if (view->focusFunc) {
466 			view->focusFunc(view, TRUE);
467 		}
468 		return DefWindowProc(view->impl->hwnd, message, wParam, lParam);
469 	case WM_KILLFOCUS:
470 		if (view->focusFunc) {
471 			view->focusFunc(view, FALSE);
472 		}
473 		return DefWindowProc(view->impl->hwnd, message, wParam, lParam);
474 	case WM_QUIT:
475 	case LOCAL_CLOSE_MSG:
476 		if (view->closeFunc) {
477 			view->closeFunc(view);
478 		}
479 		break;
480 	default:
481 		return DefWindowProc(
482 			view->impl->hwnd, message, wParam, lParam);
483 	}
484 
485 	return 0;
486 }
487 
488 PuglStatus
puglProcessEvents(PuglView * view)489 puglProcessEvents(PuglView* view)
490 {
491 	MSG msg;
492 	while (PeekMessage(&msg, view->impl->hwnd, 0, 0, PM_REMOVE)) {
493 		handleMessage(view, msg.message, msg.wParam, msg.lParam);
494 	}
495 
496 	if (view->resize) {
497 		puglResize(view);
498 	}
499 
500 	if (view->redisplay) {
501 		InvalidateRect(view->impl->hwnd, NULL, FALSE);
502 	}
503 
504 	return PUGL_SUCCESS;
505 }
506 
507 LRESULT CALLBACK
wndProc(HWND hwnd,UINT message,WPARAM wParam,LPARAM lParam)508 wndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
509 {
510 	PuglView* view = (PuglView*)GetWindowLongPtr(hwnd, GWL_USERDATA);
511 	switch (message) {
512 	case WM_CREATE:
513 		PostMessage(hwnd, WM_SHOWWINDOW, TRUE, 0);
514 		return 0;
515 	case WM_CLOSE:
516 		PostMessage(hwnd, LOCAL_CLOSE_MSG, wParam, lParam);
517 		return 0;
518 	case WM_DESTROY:
519 		return 0;
520 	default:
521 		if (view && hwnd == view->impl->hwnd) {
522 			return handleMessage(view, message, wParam, lParam);
523 		} else {
524 			return DefWindowProc(hwnd, message, wParam, lParam);
525 		}
526 	}
527 }
528 
529 void
puglPostRedisplay(PuglView * view)530 puglPostRedisplay(PuglView* view)
531 {
532 	view->redisplay = true;
533 }
534 
535 void
puglPostResize(PuglView * view)536 puglPostResize(PuglView* view)
537 {
538 	view->resize = true;
539 }
540 
541 PuglNativeWindow
puglGetNativeWindow(PuglView * view)542 puglGetNativeWindow(PuglView* view)
543 {
544 	return (PuglNativeWindow)view->impl->hwnd;
545 }
546 
547 int
puglOpenFileDialog(PuglView * view,const char * title)548 puglOpenFileDialog(PuglView* view, const char *title)
549 {
550 	char fn[1024] = "";
551 	OPENFILENAME ofn;
552 	ZeroMemory(&ofn, sizeof(OPENFILENAME));
553 	ofn.lStructSize = sizeof(OPENFILENAME);
554 	ofn.lpstrFile = fn;
555 	ofn.nMaxFile = 1024;
556 	ofn.lpstrTitle = title;
557 	ofn.lpstrFilter = "All\0*.*\0";
558 	ofn.nFilterIndex = 0;
559 	ofn.lpstrInitialDir = 0;
560 	ofn.Flags = OFN_ENABLESIZING | OFN_FILEMUSTEXIST | OFN_NONETWORKBUTTON | OFN_HIDEREADONLY | OFN_READONLY;
561 
562 	// TODO look into async ofn.lpfnHook, OFN_ENABLEHOOK
563 	// UINT_PTR CALLBACK openFileHook(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
564 	// yet it seems GetOpenFIleName itself won't return anyway.
565 
566 	ofn.hwndOwner = view->impl->hwnd; // modal
567 
568 	if (GetOpenFileName (&ofn)) {
569 		if (view->fileSelectedFunc) {
570 			view->fileSelectedFunc(view, fn);
571 		}
572 	} else {
573 		if (view->fileSelectedFunc) {
574 			view->fileSelectedFunc(view, NULL);
575 		}
576 	}
577 	return 0;
578 }
579 
580 int
puglUpdateGeometryConstraints(PuglView * view,int min_width,int min_height,bool aspect)581 puglUpdateGeometryConstraints(PuglView* view, int min_width, int min_height, bool aspect)
582 {
583 	view->min_width = min_width;
584 	view->min_height = min_height;
585 	if (view->width < min_width || view->height < min_height) {
586 		if (!view->resize) {
587 			if (view->width < min_width) view->width = min_width;
588 			if (view->height < min_height) view->height = min_height;
589 			view->resize = true;
590 			// TODO: trigger resize
591 			// (not always needed since this fn is usually called in response to a resize)
592 		}
593 	}
594 	return 0;
595 }
596