1 /** 2 * Copyright: Copyright Auburn Sounds 2015 - 2017. 3 * Copyright Richard Andrew Cattermole 2017. 4 * 5 * License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 * Authors: Guillaume Piolat 7 */ 8 module dplug.window.window; 9 10 import dplug.math.box; 11 12 import dplug.core.nogc; 13 import dplug.graphics.image; 14 15 enum Key 16 { 17 space, 18 upArrow, 19 downArrow, 20 leftArrow, 21 rightArrow, 22 digit0, 23 digit1, 24 digit2, 25 digit3, 26 digit4, 27 digit5, 28 digit6, 29 digit7, 30 digit8, 31 digit9, 32 a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w, x, y, z, 33 A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W, X, Y, Z, 34 backspace, 35 enter, 36 escape, 37 suppr, 38 unsupported // special value, means "other" 39 }; 40 41 public dchar getCharFromKey(Key key) nothrow @nogc 42 { 43 switch(key) 44 { 45 case Key.backspace: return '\t'; 46 case Key.suppr: return '\x7f'; 47 case Key.digit0: .. case Key.digit9: return cast(dchar)('0' + (key - Key.digit0)); 48 case Key.a: .. case Key.z: return cast(dchar)('a' + (key - Key.a)); 49 case Key.A: .. case Key.Z: return cast(dchar)('A' + (key - Key.A)); 50 case Key.space : return ' '; 51 default: return '\0'; 52 } 53 } 54 55 enum MouseButton 56 { 57 left, 58 right, 59 middle, 60 x1, 61 x2 62 } 63 64 struct MouseState 65 { 66 bool leftButtonDown; 67 bool rightButtonDown; 68 bool middleButtonDown; 69 bool x1ButtonDown; 70 bool x2ButtonDown; 71 bool ctrlPressed; 72 bool shiftPressed; 73 bool altPressed; 74 } 75 76 enum MouseCursor 77 { 78 /// Default cursor 79 pointer, 80 81 /// Indicates that the underlying item can be clicked like an hyperlink, and will "jump". 82 linkSelect, 83 84 /// Indicates that the underlying item can be clicked and then dragged, in no particular directions. 85 /// When an open-hand is available, this is an open-hand cursor. 86 move, 87 88 /// Indicates that clicked item can be moved in ether vertical or hozizontal directions. 89 /// When an closed-hand is available, this is a closed-hand cursor. 90 drag, 91 92 /// Indicated vertical resize abilities. 93 verticalResize, 94 95 /// Indicated horizontal resize abilities. 96 horizontalResize, 97 98 /// Indicated diagonalResize resize abilities. 99 diagonalResize, 100 101 /// Cursor is hidden 102 hidden 103 } 104 105 /// Is this window intended as a plug-in window running inside a host, 106 /// or a host window itself possibly hosting a plug-in? 107 enum WindowUsage 108 { 109 /// This window is intended to be for displaying a plugin UI. 110 /// Event pumping is done by the host (except in the X11 case where it's 111 /// done by an internal thread). 112 plugin, 113 114 /// This window is intended to be top-level, for hosting another OS window. 115 /// Event pumping will be done by the caller manually through `waitEventAndDispatch()` 116 /// Important: This case is not the nominal case. 117 /// Some calls to the `IWindowListener` will make no sense. 118 host 119 } 120 121 /// Giving commands to a window. 122 interface IWindow 123 { 124 nothrow: 125 @nogc: 126 /// To put in your message loop. 127 /// This call should only be used if the window was 128 /// created with `WindowUsage.host`. 129 /// Else, event pumping is managed by the host or internally (X11). 130 void waitEventAndDispatch(); 131 132 /// If exit was requested. 133 /// This call should only be used if the window was 134 /// created with `WindowUsage.host`. 135 /// In the case of a plug-in, the plugin client will request 136 /// termination of the window through its destructor. 137 bool terminated(); 138 139 /// Profile-purpose: get time in milliseconds. 140 /// Use the results of this function for deltas only. 141 uint getTimeMs(); 142 143 /// Gets the window's OS handle. 144 void* systemHandle(); 145 146 /// Request a resize from the native window. 147 /// If successful, onResized` should be called after with _some_ width 148 /// and height. 149 /// Note: DPI unaware. This doesn't check size constraints. 150 /// Do not call this with a size that isn't compatible with your desired 151 /// user pixel size, after GUIGraphics _userArea adjustments. 152 bool requestResize(int widthLogicalPixels, int heightLogicalPixels, bool alsoResizeParentWindow); 153 } 154 155 enum WindowPixelFormat 156 { 157 BGRA8, 158 ARGB8, 159 RGBA8 160 } 161 162 /// Receiving commands from a window. 163 /// 164 /// IMPORTANT The IWindow implementation should not call these callback without care. 165 /// In particular, there are two sets of calls that are assumed will NOT be called concurrently. 166 /// 167 /// Set 1: 168 /// - `onAnimate` 169 /// - key events 170 /// - mouse events 171 /// - `onMouseCaptureCancelled` 172 /// MUST NOT be called concurrently. 173 /// 174 /// Set 2: 175 /// - `onDraw` 176 /// - `onResized` 177 /// - `recomputeDirtyAreas` <---- this particular set has birthed many data races 178 /// - `getDirtyRectangle` 179 /// MUST NOT be called concurrently. 180 /// 181 /// Some IWindow implentation ensure that unicity through passing in an event queue, others 182 /// like the X11 implementation have to use locks. 183 /// 184 /// TODO: clarify this, additionally onDraw and onAnimate are NOT called concurrently, on purpose: 185 /// https://github.com/AuburnSounds/Dplug/issues/453 186 interface IWindowListener 187 { 188 nothrow @nogc: 189 /// Called on mouse click. 190 /// Returns: true if the event was handled. 191 bool onMouseClick(int x, int y, MouseButton mb, bool isDoubleClick, MouseState mstate); 192 193 /// Called on mouse button release 194 /// Returns: true if the event was handled. 195 bool onMouseRelease(int x, int y, MouseButton mb, MouseState mstate); 196 197 /// Called on mouse wheel movement 198 /// Returns: true if the event was handled. 199 bool onMouseWheel(int x, int y, int wheelDeltaX, int wheelDeltaY, MouseState mstate); 200 201 /// Called on mouse movement (might not be within the window) 202 void onMouseMove(int x, int y, int dx, int dy, MouseState mstate); 203 204 /// Called on keyboard press. 205 /// Returns: true if the event was handled. 206 bool onKeyDown(Key key); 207 208 /// Called on keyboard release. 209 /// Returns: true if the event was handled. 210 bool onKeyUp(Key up); 211 212 /// Render the window in software in the buffer previously returned by `onResized`. 213 /// At the end of this function, the whole buffer should be a valid, coherent UI. 214 /// 215 /// recomputeDirtyAreas() MUST have been called before this is called. 216 /// The pixel format cannot change over the lifetime of the window. 217 /// 218 /// `onDraw` guarantees the pixels to be in the format requested by `pf`, and it also 219 /// guarantees that the alpha channel will be filled with 255. 220 void onDraw(WindowPixelFormat pf); 221 222 /// The drawing area size has changed. 223 /// Always called at least once before onDraw. 224 /// Returns: the location of the full rendered framebuffer. 225 ImageRef!RGBA onResized(int width, int height); 226 227 /// Recompute internally what needs be done for the next onDraw. 228 /// This function MUST have been called before calling `onDraw` and `getDirtyRectangle`. 229 /// This method exists to allow the Window to recompute these draw lists less. 230 /// And because cache invalidation was easier on user code than internally in the UI. 231 /// Important: once you've called `recomputeDirtyAreas()` you COMMIT to redraw the 232 /// corresponding area given by `getDirtyRectangle()`. 233 /// IMPORTANT: Two calls to `recomputeDirtyAreas()` will not yield the same area. 234 /// VERY IMPORTANT: See the above note about concurrent calls. 235 void recomputeDirtyAreas(); 236 237 /// Returns: Minimal rectangle that contains dirty UIELement in UI + their graphical extent. 238 /// Empty box if nothing to update. 239 /// recomputeDirtyAreas() MUST have been called before. 240 box2i getDirtyRectangle(); 241 242 /// Called whenever mouse capture was canceled (ALT + TAB, SetForegroundWindow...) 243 void onMouseCaptureCancelled(); 244 245 /// Called whenever mouse exited the window (but a capture could still be in action). 246 void onMouseExitedWindow(); 247 248 /// Must be called periodically (ideally 60 times per second but this is not mandatory). 249 /// `time` must refer to the window creation time. 250 /// `dt` and `time` are expressed in seconds (not milliseconds). 251 void onAnimate(double dt, double time); 252 253 /// Must be called to get the current mouse cursor state for the plugin 254 MouseCursor getMouseCursor(); 255 } 256 257 /// Various backends for windowing. 258 enum WindowBackend 259 { 260 autodetect, 261 win32, 262 carbon, // Legacy, now unsupported 263 cocoa, 264 x11 265 } 266 267 /// Returns: `true` if that windowing backend is supported on this platform. 268 static isWindowBackendSupported(WindowBackend backend) nothrow @nogc 269 { 270 version(Windows) 271 return (backend == WindowBackend.win32); 272 else version(OSX) 273 { 274 version(AArch64) 275 return (backend == WindowBackend.cocoa); 276 else version(X86_64) 277 return (backend == WindowBackend.cocoa); 278 else version(X86) 279 return (backend == WindowBackend.cocoa); 280 else 281 static assert(false, "unsupported arch"); 282 } 283 else version(linux) 284 return (backend == WindowBackend.x11); 285 else 286 static assert(false, "Unsupported OS"); 287 } 288 289 290 291 /// Factory function to create windows. 292 /// 293 /// The window is allocated with `mallocNew` and should be destroyed with `destroyFree`. 294 /// 295 /// Returns: null if this backend isn't available on this platform. 296 /// 297 /// Params: 298 /// usage = Intended usage of the window. 299 /// 300 /// parentInfo = OS handle of the parent window. 301 /// For `WindowBackend.win32` it's a HWND. 302 /// For `WindowBackend.carbon` it's a NSWindow. 303 /// For `WindowBackend.x11` it's _unused_. 304 /// 305 /// controlInfo = only used in Carbon Audio Units, an additional parenting information. 306 /// Can be `null` otherwise. 307 /// 308 /// listener = A `IWindowListener` which listens to events by this window. Can be `null` for the moment. 309 /// Must outlive the created window. 310 /// 311 /// backend = Which windowing sub-system is used. Only Mac has any choice in this. 312 /// Should be `WindowBackend.autodetect` in almost all cases 313 /// 314 /// width = Initial width of the window. 315 /// 316 /// height = Initial height of the window. 317 /// 318 nothrow @nogc 319 IWindow createWindow(WindowUsage usage, 320 void* parentInfo, 321 void* controlInfo, 322 IWindowListener listener, 323 WindowBackend backend, 324 int width, 325 int height) 326 { 327 //MAYDO `null` listeners not accepted anymore. 328 //assert(listener !is null); 329 330 static WindowBackend autoDetectBackend() nothrow @nogc 331 { 332 version(Windows) 333 return WindowBackend.win32; 334 else version(OSX) 335 { 336 return WindowBackend.cocoa; 337 } 338 else version(linux) 339 { 340 return WindowBackend.x11; 341 } 342 else 343 static assert(false, "Unsupported OS"); 344 } 345 346 if (backend == WindowBackend.autodetect) 347 backend = autoDetectBackend(); 348 349 version(Windows) 350 { 351 if (backend == WindowBackend.win32) 352 { 353 import core.sys.windows.windef; 354 import dplug.window.win32window; 355 HWND parent = cast(HWND)parentInfo; 356 return mallocNew!Win32Window(parent, listener, width, height); 357 } 358 else 359 return null; 360 } 361 else version(OSX) 362 { 363 if (backend == WindowBackend.cocoa) 364 { 365 import dplug.window.cocoawindow; 366 return mallocNew!CocoaWindow(usage, parentInfo, listener, width, height); 367 } 368 else 369 return null; 370 } 371 else version(linux) 372 { 373 if (backend == WindowBackend.x11) 374 { 375 import dplug.window.x11window; 376 return mallocNew!X11Window(usage, parentInfo, listener, width, height); 377 } 378 else 379 return null; 380 } 381 else 382 { 383 static assert(false, "Unsupported OS."); 384 } 385 }