• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2014 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#if !defined(__has_feature) || !__has_feature(objc_arc)
6#error "This file requires ARC support."
7#endif
8
9#import "remoting/ios/ui/scene_view.h"
10
11#import "remoting/ios/utility.h"
12
13namespace {
14
15// TODO (aboone) Some of the layout is not yet set in stone, so variables have
16// been used to position and turn items on and off.  Eventually these may be
17// stabilized and removed.
18
19// Scroll speed multiplier for swiping
20const static int kMouseSensitivity = 2.5;
21
22// Input Axis inversion
23// 1 for standard, -1 for inverted
24const static int kXAxisInversion = -1;
25const static int kYAxisInversion = -1;
26
27// Experimental value for bounding the maximum zoom ratio
28const static int kMaxZoomSize = 3;
29}  // namespace
30
31@interface SceneView (Private)
32// Returns the number of pixels displayed per device pixel when the scaling is
33// such that the entire frame would fit perfectly in content.  Note the ratios
34// are different for width and height, some people have multiple monitors, some
35// have 16:9 or 4:3 while iPad is always single screen, but different iOS
36// devices have different resolutions.
37- (CGPoint)pixelRatio;
38
39// Return the FrameSize in perspective of the CLIENT resolution
40- (webrtc::DesktopSize)frameSizeToScale:(float)scale;
41
42// When bounded on the top and right, this point is where the scene must be
43// positioned given a scene size
44- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size;
45
46// Converts a point in the the CLIENT resolution to a similar point in the HOST
47// resolution.  Additionally, CLIENT resolution is expressed in float values
48// while HOST operates in integer values.
49- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint
50                          targetPoint:(webrtc::DesktopVector&)desktopPoint;
51
52// Converts a point in the the HOST resolution to a similar point in the CLIENT
53// resolution.  Additionally, CLIENT resolution is expressed in float values
54// while HOST operates in integer values.
55- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint
56                          targetPoint:(CGPoint&)touchPoint;
57@end
58
59@implementation SceneView
60
61- (id)init {
62  self = [super init];
63  if (self) {
64
65    _frameSize = webrtc::DesktopSize(1, 1);
66    _contentSize = webrtc::DesktopSize(1, 1);
67    _mousePosition = webrtc::DesktopVector(0, 0);
68
69    _position = GLKVector3Make(0, 0, 1);
70    _margin.left = 0;
71    _margin.right = 0;
72    _margin.top = 0;
73    _margin.bottom = 0;
74    _anchored.left = false;
75    _anchored.right = false;
76    _anchored.top = false;
77    _anchored.bottom = false;
78  }
79  return self;
80}
81
82- (const GLKMatrix4&)projectionMatrix {
83  return _projectionMatrix;
84}
85
86- (const GLKMatrix4&)modelViewMatrix {
87  // Start by using the entire scene
88  _modelViewMatrix = GLKMatrix4Identity;
89
90  // Position scene according to any panning or bounds
91  _modelViewMatrix = GLKMatrix4Translate(_modelViewMatrix,
92                                         _position.x + _margin.left,
93                                         _position.y + _margin.bottom,
94                                         0.0);
95
96  // Apply zoom
97  _modelViewMatrix = GLKMatrix4Scale(_modelViewMatrix,
98                                     _position.z / self.pixelRatio.x,
99                                     _position.z / self.pixelRatio.y,
100                                     1.0);
101
102  // We are directly above the screen and looking down.
103  static const GLKMatrix4 viewMatrix = GLKMatrix4MakeLookAt(
104      0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0);  // center view
105
106  _modelViewMatrix = GLKMatrix4Multiply(viewMatrix, _modelViewMatrix);
107
108  return _modelViewMatrix;
109}
110
111- (const webrtc::DesktopSize&)contentSize {
112  return _contentSize;
113}
114
115- (void)setContentSize:(const CGSize&)size {
116
117  _contentSize.set(size.width, size.height);
118
119  _projectionMatrix = GLKMatrix4MakeOrtho(
120      0.0, _contentSize.width(), 0.0, _contentSize.height(), 1.0, -1.0);
121
122  TexturedQuad newQuad;
123  newQuad.bl.geometryVertex = CGPointMake(0.0, 0.0);
124  newQuad.br.geometryVertex = CGPointMake(_contentSize.width(), 0.0);
125  newQuad.tl.geometryVertex = CGPointMake(0.0, _contentSize.height());
126  newQuad.tr.geometryVertex =
127      CGPointMake(_contentSize.width(), _contentSize.height());
128
129  newQuad.bl.textureVertex = CGPointMake(0.0, 1.0);
130  newQuad.br.textureVertex = CGPointMake(1.0, 1.0);
131  newQuad.tl.textureVertex = CGPointMake(0.0, 0.0);
132  newQuad.tr.textureVertex = CGPointMake(1.0, 0.0);
133
134  _glQuad = newQuad;
135}
136
137- (const webrtc::DesktopSize&)frameSize {
138  return _frameSize;
139}
140
141- (void)setFrameSize:(const webrtc::DesktopSize&)size {
142  DCHECK(size.width() > 0 && size.height() > 0);
143  // Don't do anything if the size has not changed.
144  if (_frameSize.equals(size))
145    return;
146
147  _frameSize.set(size.width(), size.height());
148
149  _position.x = 0;
150  _position.y = 0;
151
152  float verticalPixelScaleRatio =
153      (static_cast<float>(_contentSize.height() - _margin.top -
154                          _margin.bottom) /
155       static_cast<float>(_frameSize.height())) /
156      _position.z;
157
158  // Anchored at the position (0,0)
159  _anchored.left = YES;
160  _anchored.right = NO;
161  _anchored.top = NO;
162  _anchored.bottom = YES;
163
164  [self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:verticalPixelScaleRatio];
165
166  // Center the mouse on the CLIENT screen
167  webrtc::DesktopVector centerMouseLocation;
168  if ([self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2,
169                                                      _contentSize.height() / 2)
170                              targetPoint:centerMouseLocation]) {
171    _mousePosition.set(centerMouseLocation.x(), centerMouseLocation.y());
172  }
173
174#if DEBUG
175  NSLog(@"resized frame:%d:%d scale:%f",
176        _frameSize.width(),
177        _frameSize.height(),
178        _position.z);
179#endif  // DEBUG
180}
181
182- (const webrtc::DesktopVector&)mousePosition {
183  return _mousePosition;
184}
185
186- (void)setPanVelocity:(const CGPoint&)delta {
187  _panVelocity.x = delta.x;
188  _panVelocity.y = delta.y;
189}
190
191- (void)setMarginsFromLeft:(int)left
192                     right:(int)right
193                       top:(int)top
194                    bottom:(int)bottom {
195  _margin.left = left;
196  _margin.right = right;
197  _margin.top = top;
198  _margin.bottom = bottom;
199}
200
201- (void)draw {
202  glEnableVertexAttribArray(GLKVertexAttribPosition);
203  glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
204  glEnableVertexAttribArray(GLKVertexAttribTexCoord1);
205
206  // Define our scene space
207  glVertexAttribPointer(GLKVertexAttribPosition,
208                        2,
209                        GL_FLOAT,
210                        GL_FALSE,
211                        sizeof(TexturedVertex),
212                        &(_glQuad.bl.geometryVertex));
213  // Define the desktop plane
214  glVertexAttribPointer(GLKVertexAttribTexCoord0,
215                        2,
216                        GL_FLOAT,
217                        GL_FALSE,
218                        sizeof(TexturedVertex),
219                        &(_glQuad.bl.textureVertex));
220  // Define the cursor plane
221  glVertexAttribPointer(GLKVertexAttribTexCoord1,
222                        2,
223                        GL_FLOAT,
224                        GL_FALSE,
225                        sizeof(TexturedVertex),
226                        &(_glQuad.bl.textureVertex));
227
228  // Draw!
229  glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
230
231  [Utility logGLErrorCode:@"SceneView draw"];
232}
233
234- (CGPoint)pixelRatio {
235
236  CGPoint r = CGPointMake(static_cast<float>(_contentSize.width()) /
237                              static_cast<float>(_frameSize.width()),
238                          static_cast<float>(_contentSize.height()) /
239                              static_cast<float>(_frameSize.height()));
240  return r;
241}
242
243- (webrtc::DesktopSize)frameSizeToScale:(float)scale {
244  return webrtc::DesktopSize(_frameSize.width() * scale,
245                             _frameSize.height() * scale);
246}
247
248- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size {
249  webrtc::DesktopVector r(
250      _contentSize.width() - _margin.left - _margin.right - size.width(),
251      _contentSize.height() - _margin.bottom - _margin.top - size.height());
252
253  if (r.x() > 0) {
254    r.set((_contentSize.width() - size.width()) / 2, r.y());
255  }
256
257  if (r.y() > 0) {
258    r.set(r.x(), (_contentSize.height() - size.height()) / 2);
259  }
260
261  return r;
262}
263
264- (BOOL)containsTouchPoint:(CGPoint)point {
265  // Here frame is from the top-left corner, most other calculations are framed
266  // from the bottom left.
267  CGRect frame =
268      CGRectMake(_margin.left,
269                 _margin.top,
270                 _contentSize.width() - _margin.left - _margin.right,
271                 _contentSize.height() - _margin.top - _margin.bottom);
272  return CGRectContainsPoint(frame, point);
273}
274
275- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint
276                          targetPoint:(webrtc::DesktopVector&)mousePoint {
277  if (![self containsTouchPoint:touchPoint]) {
278    return NO;
279  }
280  // A touch location occurs in respect to the user's entire view surface.
281
282  // The GL Context is upside down from the User's perspective so flip it.
283  CGPoint glOrientedTouchPoint =
284      CGPointMake(touchPoint.x, _contentSize.height() - touchPoint.y);
285
286  // The GL surface generally is not at the same origination point as the touch,
287  // so translate by the scene's position.
288  CGPoint glOrientedPointInRespectToFrame =
289      CGPointMake(glOrientedTouchPoint.x - _position.x,
290                  glOrientedTouchPoint.y - _position.y);
291
292  // The perspective exists in relative to the CLIENT resolution at 1:1, zoom
293  // our perspective so we are relative to the HOST at 1:1
294  CGPoint glOrientedPointInFrame =
295      CGPointMake(glOrientedPointInRespectToFrame.x / _position.z,
296                  glOrientedPointInRespectToFrame.y / _position.z);
297
298  // Finally, flip the perspective back over to the Users, but this time in
299  // respect to the HOST desktop.  Floor to ensure the result is always in
300  // frame.
301  CGPoint deskTopOrientedPointInFrame =
302      CGPointMake(floorf(glOrientedPointInFrame.x),
303                  floorf(_frameSize.height() - glOrientedPointInFrame.y));
304
305  // Convert from float to integer
306  mousePoint.set(deskTopOrientedPointInFrame.x, deskTopOrientedPointInFrame.y);
307
308  return CGRectContainsPoint(
309      CGRectMake(0, 0, _frameSize.width(), _frameSize.height()),
310      deskTopOrientedPointInFrame);
311}
312
313- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint
314                          targetPoint:(CGPoint&)touchPoint {
315  // A mouse point is in respect to the desktop frame.
316
317  // Flip the perspective back over to the Users, in
318  // respect to the HOST desktop.
319  CGPoint deskTopOrientedPointInFrame =
320      CGPointMake(mousePoint.x(), _frameSize.height() - mousePoint.y());
321
322  // The perspective exists in relative to the CLIENT resolution at 1:1, zoom
323  // our perspective so we are relative to the HOST at 1:1
324  CGPoint glOrientedPointInFrame =
325      CGPointMake(deskTopOrientedPointInFrame.x * _position.z,
326                  deskTopOrientedPointInFrame.y * _position.z);
327
328  // The GL surface generally is not at the same origination point as the touch,
329  // so translate by the scene's position.
330  CGPoint glOrientedPointInRespectToFrame =
331      CGPointMake(glOrientedPointInFrame.x + _position.x,
332                  glOrientedPointInFrame.y + _position.y);
333
334  // Convert from float to integer
335  touchPoint.x = floorf(glOrientedPointInRespectToFrame.x);
336  touchPoint.y = floorf(glOrientedPointInRespectToFrame.y);
337
338  return [self containsTouchPoint:touchPoint];
339}
340
341- (void)panAndZoom:(CGPoint)translation scaleBy:(float)ratio {
342  CGPoint ratios = [self pixelRatio];
343
344  // New Scaling factor bounded by a min and max
345  float resultScale = _position.z * ratio;
346  float scaleUpperBound = MAX(ratios.x, MAX(ratios.y, kMaxZoomSize));
347  float scaleLowerBound = MIN(ratios.x, ratios.y);
348
349  if (resultScale < scaleLowerBound) {
350    resultScale = scaleLowerBound;
351  } else if (resultScale > scaleUpperBound) {
352    resultScale = scaleUpperBound;
353  }
354
355  DCHECK(isnormal(resultScale) && resultScale > 0);
356
357  // The GL perspective is upside down in relation to the User's view, so flip
358  // the translation
359  translation.y = -translation.y;
360
361  // The constants here could be user options later.
362  translation.x =
363      translation.x * kXAxisInversion * (1 / (ratios.x * kMouseSensitivity));
364  translation.y =
365      translation.y * kYAxisInversion * (1 / (ratios.y * kMouseSensitivity));
366
367  CGPoint delta = CGPointMake(0, 0);
368  CGPoint scaleDelta = CGPointMake(0, 0);
369
370  webrtc::DesktopSize currentSize = [self frameSizeToScale:_position.z];
371
372  {
373    // Closure for this variable, so the variable is not available to the rest
374    // of this function
375    webrtc::DesktopVector currentBounds = [self getBoundsForSize:currentSize];
376    // There are rounding errors in the scope of this function, see the
377    // butterfly effect.  In successive calls, the resulting position isn't
378    // always exactly the calculated position. If we know we are Anchored, then
379    // go ahead and reposition it to the values above.
380    if (_anchored.right) {
381      _position.x = currentBounds.x();
382    }
383
384    if (_anchored.top) {
385      _position.y = currentBounds.y();
386    }
387  }
388
389  if (_position.z != resultScale) {
390    // When scaling the scene, the origination of scaling is the mouse's
391    // location.  But when the frame is anchored, adjust the origination to the
392    // anchor point.
393
394    CGPoint mousePositionInClientResolution;
395    [self convertMousePointToTouchPoint:_mousePosition
396                            targetPoint:mousePositionInClientResolution];
397
398    // Prefer to zoom based on the left anchor when there is a choice
399    if (_anchored.left) {
400      mousePositionInClientResolution.x = 0;
401    } else if (_anchored.right) {
402      mousePositionInClientResolution.x = _contentSize.width();
403    }
404
405    // Prefer to zoom out from the top anchor when there is a choice
406    if (_anchored.top) {
407      mousePositionInClientResolution.y = _contentSize.height();
408    } else if (_anchored.bottom) {
409      mousePositionInClientResolution.y = 0;
410    }
411
412    scaleDelta.x -=
413        [SceneView positionDeltaFromScaling:ratio
414                                   position:_position.x
415                                     length:currentSize.width()
416                                     anchor:mousePositionInClientResolution.x];
417
418    scaleDelta.y -=
419        [SceneView positionDeltaFromScaling:ratio
420                                   position:_position.y
421                                     length:currentSize.height()
422                                     anchor:mousePositionInClientResolution.y];
423  }
424
425  delta.x = [SceneView
426      positionDeltaFromTranslation:translation.x
427                          position:_position.x
428                         freeSpace:_contentSize.width() - currentSize.width()
429             scaleingPositionDelta:scaleDelta.x
430                     isAnchoredLow:_anchored.left
431                    isAnchoredHigh:_anchored.right];
432
433  delta.y = [SceneView
434      positionDeltaFromTranslation:translation.y
435                          position:_position.y
436                         freeSpace:_contentSize.height() - currentSize.height()
437             scaleingPositionDelta:scaleDelta.y
438                     isAnchoredLow:_anchored.bottom
439                    isAnchoredHigh:_anchored.top];
440  {
441    // Closure for this variable, so the variable is not available to the rest
442    // of this function
443    webrtc::DesktopVector bounds =
444        [self getBoundsForSize:[self frameSizeToScale:resultScale]];
445
446    delta.x = [SceneView boundDeltaFromPosition:_position.x
447                                          delta:delta.x
448                                     lowerBound:bounds.x()
449                                     upperBound:0];
450
451    delta.y = [SceneView boundDeltaFromPosition:_position.y
452                                          delta:delta.y
453                                     lowerBound:bounds.y()
454                                     upperBound:0];
455  }
456
457  BOOL isLeftAndRightAnchored = _anchored.left && _anchored.right;
458  BOOL isTopAndBottomAnchored = _anchored.top && _anchored.bottom;
459
460  [self updateMousePositionAndAnchorsWithTranslation:translation
461                                               scale:resultScale];
462
463  // If both anchors were lost, then keep the one that is easier to predict
464  if (isLeftAndRightAnchored && !_anchored.left && !_anchored.right) {
465    delta.x = -_position.x;
466    _anchored.left = YES;
467  }
468
469  // If both anchors were lost, then keep the one that is easier to predict
470  if (isTopAndBottomAnchored && !_anchored.top && !_anchored.bottom) {
471    delta.y = -_position.y;
472    _anchored.bottom = YES;
473  }
474
475  // FINALLY, update the scene's position
476  _position.x += delta.x;
477  _position.y += delta.y;
478  _position.z = resultScale;
479}
480
481- (void)updateMousePositionAndAnchorsWithTranslation:(CGPoint)translation
482                                               scale:(float)scale {
483  webrtc::DesktopVector centerMouseLocation;
484  [self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2,
485                                                  _contentSize.height() / 2)
486                          targetPoint:centerMouseLocation];
487
488  webrtc::DesktopVector currentBounds =
489      [self getBoundsForSize:[self frameSizeToScale:_position.z]];
490  webrtc::DesktopVector nextBounds =
491      [self getBoundsForSize:[self frameSizeToScale:scale]];
492
493  webrtc::DesktopVector predictedMousePosition(
494      _mousePosition.x() - translation.x, _mousePosition.y() + translation.y);
495
496  _mousePosition.set(
497      [SceneView boundMouseGivenNextPosition:predictedMousePosition.x()
498                                 maxPosition:_frameSize.width()
499                              centerPosition:centerMouseLocation.x()
500                               isAnchoredLow:_anchored.left
501                              isAnchoredHigh:_anchored.right],
502      [SceneView boundMouseGivenNextPosition:predictedMousePosition.y()
503                                 maxPosition:_frameSize.height()
504                              centerPosition:centerMouseLocation.y()
505                               isAnchoredLow:_anchored.top
506                              isAnchoredHigh:_anchored.bottom]);
507
508  _panVelocity.x = [SceneView boundVelocity:_panVelocity.x
509                                 axisLength:_frameSize.width()
510                              mousePosition:_mousePosition.x()];
511  _panVelocity.y = [SceneView boundVelocity:_panVelocity.y
512                                 axisLength:_frameSize.height()
513                              mousePosition:_mousePosition.y()];
514
515  _anchored.left = (nextBounds.x() >= 0) ||
516                   (_position.x == 0 &&
517                    predictedMousePosition.x() <= centerMouseLocation.x());
518
519  _anchored.right =
520      (nextBounds.x() >= 0) ||
521      (_position.x == currentBounds.x() &&
522       predictedMousePosition.x() >= centerMouseLocation.x()) ||
523      (_mousePosition.x() == _frameSize.width() - 1 && !_anchored.left);
524
525  _anchored.bottom = (nextBounds.y() >= 0) ||
526                     (_position.y == 0 &&
527                      predictedMousePosition.y() >= centerMouseLocation.y());
528
529  _anchored.top =
530      (nextBounds.y() >= 0) ||
531      (_position.y == currentBounds.y() &&
532       predictedMousePosition.y() <= centerMouseLocation.y()) ||
533      (_mousePosition.y() == _frameSize.height() - 1 && !_anchored.bottom);
534}
535
536+ (float)positionDeltaFromScaling:(float)ratio
537                         position:(float)position
538                           length:(float)length
539                           anchor:(float)anchor {
540  float newSize = length * ratio;
541  float scaleXBy = fabs(position - anchor) / length;
542  float delta = (newSize - length) * scaleXBy;
543  return delta;
544}
545
546+ (int)positionDeltaFromTranslation:(int)translation
547                           position:(int)position
548                          freeSpace:(int)freeSpace
549              scaleingPositionDelta:(int)scaleingPositionDelta
550                      isAnchoredLow:(BOOL)isAnchoredLow
551                     isAnchoredHigh:(BOOL)isAnchoredHigh {
552  if (isAnchoredLow && isAnchoredHigh) {
553    // center the view
554    return (freeSpace / 2) - position;
555  } else if (isAnchoredLow) {
556    return 0;
557  } else if (isAnchoredHigh) {
558    return scaleingPositionDelta;
559  } else {
560    return translation + scaleingPositionDelta;
561  }
562}
563
564+ (int)boundDeltaFromPosition:(float)position
565                        delta:(int)delta
566                   lowerBound:(int)lowerBound
567                   upperBound:(int)upperBound {
568  int result = position + delta;
569
570  if (lowerBound < upperBound) {  // the view is larger than the bounds
571    if (result > upperBound) {
572      result = upperBound;
573    } else if (result < lowerBound) {
574      result = lowerBound;
575    }
576  } else {
577    // the view is smaller than the bounds so we'll always be at the lowerBound
578    result = lowerBound;
579  }
580  return result - position;
581}
582
583+ (int)boundMouseGivenNextPosition:(int)nextPosition
584                       maxPosition:(int)maxPosition
585                    centerPosition:(int)centerPosition
586                     isAnchoredLow:(BOOL)isAnchoredLow
587                    isAnchoredHigh:(BOOL)isAnchoredHigh {
588  if (nextPosition < 0) {
589    return 0;
590  }
591  if (nextPosition > maxPosition - 1) {
592    return maxPosition - 1;
593  }
594
595  if ((isAnchoredLow && nextPosition <= centerPosition) ||
596      (isAnchoredHigh && nextPosition >= centerPosition)) {
597    return nextPosition;
598  }
599
600  return centerPosition;
601}
602
603+ (float)boundVelocity:(float)velocity
604            axisLength:(int)axisLength
605         mousePosition:(int)mousePosition {
606  if (velocity != 0) {
607    if (mousePosition <= 0 || mousePosition >= (axisLength - 1)) {
608      return 0;
609    }
610  }
611
612  return velocity;
613}
614
615- (BOOL)tickPanVelocity {
616  BOOL inMotion = ((_panVelocity.x != 0.0) || (_panVelocity.y != 0.0));
617
618  if (inMotion) {
619
620    uint32_t divisor = 50 / _position.z;
621    float reducer = .95;
622
623    if (_panVelocity.x != 0.0 && ABS(_panVelocity.x) < divisor) {
624      _panVelocity = CGPointMake(0.0, _panVelocity.y);
625    }
626
627    if (_panVelocity.y != 0.0 && ABS(_panVelocity.y) < divisor) {
628      _panVelocity = CGPointMake(_panVelocity.x, 0.0);
629    }
630
631    [self panAndZoom:CGPointMake(_panVelocity.x / divisor,
632                                 _panVelocity.y / divisor)
633             scaleBy:1.0];
634
635    _panVelocity.x *= reducer;
636    _panVelocity.y *= reducer;
637  }
638
639  return inMotion;
640}
641
642@end