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 }