1 /* ScummVM - Graphic Adventure Engine
2  *
3  * ScummVM is the legal property of its developers, whose names
4  * are too numerous to list here. Please refer to the COPYRIGHT
5  * file distributed with this source distribution.
6  *
7  * This program is free software; you can redistribute it and/or
8  * modify it under the terms of the GNU General Public License
9  * as published by the Free Software Foundation; either version 2
10  * of the License, or (at your option) any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
20  *
21  */
22 
23 #ifndef BACKENDS_GRAPHICS_WINDOWED_H
24 #define BACKENDS_GRAPHICS_WINDOWED_H
25 
26 #include "backends/graphics/graphics.h"
27 #include "common/frac.h"
28 #include "common/rect.h"
29 #include "common/config-manager.h"
30 #include "common/textconsole.h"
31 #include "graphics/scaler/aspect.h"
32 
33 enum {
34 	STRETCH_CENTER = 0,
35 	STRETCH_INTEGRAL = 1,
36 	STRETCH_INTEGRAL_AR = 2,
37 	STRETCH_FIT = 3,
38 	STRETCH_STRETCH = 4,
39 	STRETCH_FIT_FORCE_ASPECT = 5
40 };
41 
42 class WindowedGraphicsManager : virtual public GraphicsManager {
43 public:
WindowedGraphicsManager()44 	WindowedGraphicsManager() :
45 		_windowWidth(0),
46 		_windowHeight(0),
47 		_overlayVisible(false),
48 		_gameScreenShakeXOffset(0),
49 		_gameScreenShakeYOffset(0),
50 		_forceRedraw(false),
51 		_cursorVisible(false),
52 		_cursorX(0),
53 		_cursorY(0),
54 		_cursorNeedsRedraw(false),
55 		_cursorLastInActiveArea(true) {}
56 
showOverlay()57 	virtual void showOverlay() override {
58 		if (_overlayVisible)
59 			return;
60 
61 		_activeArea.drawRect = _overlayDrawRect;
62 		_activeArea.width = getOverlayWidth();
63 		_activeArea.height = getOverlayHeight();
64 		_overlayVisible = true;
65 		_forceRedraw = true;
66 	}
67 
hideOverlay()68 	virtual void hideOverlay() override {
69 		if (!_overlayVisible)
70 			return;
71 
72 		_activeArea.drawRect = _gameDrawRect;
73 		_activeArea.width = getWidth();
74 		_activeArea.height = getHeight();
75 		_overlayVisible = false;
76 		_forceRedraw = true;
77 	}
78 
isOverlayVisible()79 	virtual bool isOverlayVisible() const override { return _overlayVisible; }
80 
setShakePos(int shakeXOffset,int shakeYOffset)81 	virtual void setShakePos(int shakeXOffset, int shakeYOffset) override {
82 		if (_gameScreenShakeXOffset != shakeXOffset || _gameScreenShakeYOffset != shakeYOffset) {
83 			_gameScreenShakeXOffset = shakeXOffset;
84 			_gameScreenShakeYOffset = shakeYOffset;
85 			recalculateDisplayAreas();
86 			_cursorNeedsRedraw = true;
87 		}
88 	}
89 
getWindowWidth()90 	int getWindowWidth() const { return _windowWidth; }
getWindowHeight()91 	int getWindowHeight() const { return _windowHeight; }
92 
93 protected:
94 	/**
95 	 * @returns whether or not the game screen must have aspect ratio correction
96 	 * applied for correct rendering.
97 	 */
98 	virtual bool gameNeedsAspectRatioCorrection() const = 0;
99 
100 	/**
101 	 * Backend-specific implementation for updating internal surfaces that need
102 	 * to reflect the new window size.
103 	 */
104 	virtual void handleResizeImpl(const int width, const int height) = 0;
105 
106 	/**
107 	 * Converts the given point from the active virtual screen's coordinate
108 	 * space to the window's coordinate space (i.e. game-to-window or
109 	 * overlay-to-window).
110 	 */
convertVirtualToWindow(const int x,const int y)111 	Common::Point convertVirtualToWindow(const int x, const int y) const {
112 		const int targetX = _activeArea.drawRect.left;
113 		const int targetY = _activeArea.drawRect.top;
114 		const int targetWidth = _activeArea.drawRect.width();
115 		const int targetHeight = _activeArea.drawRect.height();
116 		const int sourceWidth = _activeArea.width;
117 		const int sourceHeight = _activeArea.height;
118 
119 		if (sourceWidth == 0 || sourceHeight == 0) {
120 			error("convertVirtualToWindow called without a valid draw rect");
121 		}
122 
123 		int windowX = targetX + (x * targetWidth + sourceWidth / 2) / sourceWidth;
124 		int windowY = targetY + (y * targetHeight + sourceHeight / 2) / sourceHeight;
125 
126 		return Common::Point(CLIP<int>(windowX, targetX, targetX + targetWidth - 1),
127 		                     CLIP<int>(windowY, targetY, targetY + targetHeight - 1));
128 	}
129 
130 	/**
131 	 * Converts the given point from the window's coordinate space to the
132 	 * active virtual screen's coordinate space (i.e. window-to-game or
133 	 * window-to-overlay).
134 	 */
convertWindowToVirtual(int x,int y)135 	Common::Point convertWindowToVirtual(int x, int y) const {
136 		const int sourceX = _activeArea.drawRect.left;
137 		const int sourceY = _activeArea.drawRect.top;
138 		const int sourceMaxX = _activeArea.drawRect.right - 1;
139 		const int sourceMaxY = _activeArea.drawRect.bottom - 1;
140 		const int sourceWidth = _activeArea.drawRect.width();
141 		const int sourceHeight = _activeArea.drawRect.height();
142 		const int targetWidth = _activeArea.width;
143 		const int targetHeight = _activeArea.height;
144 
145 		if (sourceWidth == 0 || sourceHeight == 0) {
146 			error("convertWindowToVirtual called without a valid draw rect");
147 		}
148 
149 		x = CLIP<int>(x, sourceX, sourceMaxX);
150 		y = CLIP<int>(y, sourceY, sourceMaxY);
151 
152 		int virtualX = ((x - sourceX) * targetWidth + sourceWidth / 2) / sourceWidth;
153 		int virtualY = ((y - sourceY) * targetHeight + sourceHeight / 2) / sourceHeight;
154 
155 		return Common::Point(CLIP<int>(virtualX, 0, targetWidth - 1),
156 		                     CLIP<int>(virtualY, 0, targetHeight - 1));
157 	}
158 
159 	/**
160 	 * @returns the desired aspect ratio of the game surface.
161 	 */
getDesiredGameAspectRatio()162 	frac_t getDesiredGameAspectRatio() const {
163 		if (getHeight() == 0 || gameNeedsAspectRatioCorrection()) {
164 			return intToFrac(4) / 3;
165 		}
166 
167 		return intToFrac(getWidth()) / getHeight();
168 	}
169 
170 	/**
171 	 * @returns the scale used between the game size and the surface on which it is rendered.
172 	 */
getGameRenderScale()173 	virtual int getGameRenderScale() const {
174 		return 1;
175 	}
176 
177 	/**
178 	 * Called after the window has been updated with new dimensions.
179 	 *
180 	 * @param width The new width of the window, excluding window decoration.
181 	 * @param height The new height of the window, excluding window decoration.
182 	 */
handleResize(const int width,const int height)183 	void handleResize(const int width, const int height) {
184 		_windowWidth = width;
185 		_windowHeight = height;
186 		handleResizeImpl(width, height);
187 	}
188 
189 	/**
190 	 * Recalculates the display areas for the game and overlay surfaces within
191 	 * the window.
192 	 */
recalculateDisplayAreas()193 	virtual void recalculateDisplayAreas() {
194 		if (_windowHeight == 0) {
195 			return;
196 		}
197 
198 		populateDisplayAreaDrawRect(getDesiredGameAspectRatio(), getWidth() * getGameRenderScale(), getHeight() * getGameRenderScale(), _gameDrawRect);
199 
200 		if (getOverlayHeight()) {
201 			const frac_t overlayAspect = intToFrac(getOverlayWidth()) / getOverlayHeight();
202 			populateDisplayAreaDrawRect(overlayAspect, getOverlayWidth(), getOverlayHeight(), _overlayDrawRect);
203 		}
204 
205 		if (_overlayVisible) {
206 			_activeArea.drawRect = _overlayDrawRect;
207 			_activeArea.width = getOverlayWidth();
208 			_activeArea.height = getOverlayHeight();
209 		} else {
210 			_activeArea.drawRect = _gameDrawRect;
211 			_activeArea.width = getWidth();
212 			_activeArea.height = getHeight();
213 		}
214 	}
215 
216 	/**
217 	 * Sets the position of the hardware mouse cursor in the host system,
218 	 * relative to the window.
219 	 *
220 	 * @param x X coordinate in window coordinates.
221 	 * @param y Y coordinate in window coordinates.
222 	 */
223 	virtual void setSystemMousePosition(const int x, const int y) = 0;
224 
showMouse(bool visible)225 	virtual bool showMouse(bool visible) override {
226 		if (_cursorVisible == visible) {
227 			return visible;
228 		}
229 
230 		const bool last = _cursorVisible;
231 		_cursorVisible = visible;
232 		_cursorNeedsRedraw = true;
233 		return last;
234 	}
235 
236 	/**
237 	 * Move ("warp") the mouse cursor to the specified position.
238 	 *
239 	 * @param x	The new X position of the mouse in virtual screen coordinates.
240 	 * @param y	The new Y position of the mouse in virtual screen coordinates.
241 	 */
warpMouse(int x,int y)242 	void warpMouse(int x, int y) override {
243 		// Check active coordinate instead of window coordinate to avoid warping
244 		// the mouse if it is still within the same virtual pixel
245 		const Common::Point virtualCursor = convertWindowToVirtual(_cursorX, _cursorY);
246 		if (virtualCursor.x != x || virtualCursor.y != y) {
247 			// Warping the mouse in SDL generates a mouse movement event, so
248 			// `setMousePosition` would be called eventually through the
249 			// `notifyMousePosition` callback if we *only* set the system mouse
250 			// position here. However, this can cause problems with some games.
251 			// For example, the cannon script in CoMI calls to warp the mouse
252 			// twice each time the cannon is reloaded, and unless we update the
253 			// mouse position immediately, the second call is ignored, which
254 			// causes the cannon to change its aim.
255 			const Common::Point windowCursor = convertVirtualToWindow(x, y);
256 			setMousePosition(windowCursor.x, windowCursor.y);
257 			setSystemMousePosition(windowCursor.x, windowCursor.y);
258 		}
259 	}
260 
261 	/**
262 	 * Sets the position of the rendered mouse cursor in the window.
263 	 *
264 	 * @param x X coordinate in window coordinates.
265 	 * @param y Y coordinate in window coordinates.
266 	 */
setMousePosition(int x,int y)267 	void setMousePosition(int x, int y) {
268 		if (_cursorX != x || _cursorY != y) {
269 			_cursorNeedsRedraw = true;
270 		}
271 
272 		_cursorX = x;
273 		_cursorY = y;
274 	}
275 
276 	/**
277 	 * The width of the window, excluding window decoration.
278 	 */
279 	int _windowWidth;
280 
281 	/**
282 	 * The height of the window, excluding window decoration.
283 	 */
284 	int _windowHeight;
285 
286 	/**
287 	 * Whether the overlay (i.e. launcher, including the out-of-game launcher)
288 	 * is visible or not.
289 	 */
290 	bool _overlayVisible;
291 
292 	/**
293 	 * The offset by which the screen is moved horizontally.
294 	 */
295 	int _gameScreenShakeXOffset;
296 
297 	/**
298 	* The offset by which the screen is moved vertically.
299 	*/
300 	int _gameScreenShakeYOffset;
301 
302 	/**
303 	 * The scaled draw rectangle for the game surface within the window.
304 	 */
305 	Common::Rect _gameDrawRect;
306 
307 	/**
308 	 * The scaled draw rectangle for the overlay (launcher) surface within the
309 	 * window.
310 	 */
311 	Common::Rect _overlayDrawRect;
312 
313 	/**
314 	 * Data about the display area of a virtual screen.
315 	 */
316 	struct DisplayArea {
317 		/**
318 		 * The scaled area where the virtual screen is drawn within the window.
319 		 */
320 		Common::Rect drawRect;
321 
322 		/**
323 		 * The width of the virtual screen's unscaled coordinate space.
324 		 */
325 		int width;
326 
327 		/**
328 		 * The height of the virtual screen's unscaled coordinate space.
329 		 */
330 		int height;
331 	};
332 
333 	/**
334 	 * Display area information about the currently active virtual screen. This
335 	 * will be the overlay screen when the overlay is active, and the game
336 	 * screen otherwise.
337 	 */
338 	DisplayArea _activeArea;
339 
340 	/**
341 	 * Whether the screen must be redrawn on the next frame.
342 	 */
343 	bool _forceRedraw;
344 
345 	/**
346 	 * Whether the cursor is actually visible.
347 	 */
348 	bool _cursorVisible;
349 
350 	/**
351 	 * Whether the mouse cursor needs to be redrawn on the next frame.
352 	 */
353 	bool _cursorNeedsRedraw;
354 
355 	/**
356 	 * Whether the last position of the system cursor was within the active area
357 	 * of the window.
358 	 */
359 	bool _cursorLastInActiveArea;
360 
361 	/**
362 	 * The position of the mouse cursor, in window coordinates.
363 	 */
364 	int _cursorX, _cursorY;
365 
366 private:
populateDisplayAreaDrawRect(const frac_t displayAspect,int originalWidth,int originalHeight,Common::Rect & drawRect)367 	void populateDisplayAreaDrawRect(const frac_t displayAspect, int originalWidth, int originalHeight, Common::Rect &drawRect) const {
368 		int mode = getStretchMode();
369 		// Mode Center   = use original size, or divide by an integral amount if window is smaller than game surface
370 		// Mode Integral = scale by an integral amount.
371 		// Mode Fit      = scale to fit the window while respecting the aspect ratio
372 		// Mode Stretch  = scale and stretch to fit the window without respecting the aspect ratio
373 		// Mode Fit Force Aspect = scale to fit the window while forcing a 4:3 aspect ratio
374 
375 		int width = 0, height = 0;
376 		if (mode == STRETCH_CENTER || mode == STRETCH_INTEGRAL || mode == STRETCH_INTEGRAL_AR) {
377 			width = originalWidth;
378 			height = intToFrac(width) / displayAspect;
379 			if (width > _windowWidth || height > _windowHeight) {
380 				int fac = 1 + MAX((width - 1) / _windowWidth, (height - 1) / _windowHeight);
381 				width /= fac;
382 				height /= fac;
383 			} else if (mode == STRETCH_INTEGRAL) {
384 				int fac = MIN(_windowWidth / width, _windowHeight / height);
385 				width *= fac;
386 				height *= fac;
387 			}  else if (mode == STRETCH_INTEGRAL_AR) {
388 				int targetHeight = height;
389 				int horizontalFac = _windowWidth / width;
390 				do {
391 					width = originalWidth * horizontalFac;
392 					int verticalFac = (targetHeight * horizontalFac + originalHeight / 2) / originalHeight;
393 					height = originalHeight * verticalFac;
394 					--horizontalFac;
395 				} while (horizontalFac > 0 && height > _windowHeight);
396 				if (height > _windowHeight)
397 					height = targetHeight;
398 			}
399 		} else {
400 			frac_t windowAspect = intToFrac(_windowWidth) / _windowHeight;
401 			width = _windowWidth;
402 			height = _windowHeight;
403 			if (mode == STRETCH_FIT_FORCE_ASPECT) {
404 				frac_t ratio = intToFrac(4) / 3;
405 				if (windowAspect < ratio)
406 					height = intToFrac(width) / ratio;
407 				else if (windowAspect > ratio)
408 					width = fracToInt(height * ratio);
409 			} else if (mode != STRETCH_STRETCH) {
410 				if (windowAspect < displayAspect)
411 					height = intToFrac(width) / displayAspect;
412 				else if (windowAspect > displayAspect)
413 					width = fracToInt(height * displayAspect);
414 			}
415 		}
416 
417 		drawRect.left = ((_windowWidth - width) / 2) + _gameScreenShakeXOffset * width / getWidth();
418 		drawRect.top = ((_windowHeight - height) / 2) + _gameScreenShakeYOffset * height / getHeight();
419 		drawRect.setWidth(width);
420 		drawRect.setHeight(height);
421 	}
422 };
423 
424 #endif
425