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