blob: 97a577df775849732402ac4809cc22b272e617d8 [file] [log] [blame]
// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#if !defined(__has_feature) || !__has_feature(objc_arc)
#error "This file requires ARC support."
#endif
#import "remoting/ios/ui/scene_view.h"
#import "remoting/ios/utility.h"
namespace {
// TODO (aboone) Some of the layout is not yet set in stone, so variables have
// been used to position and turn items on and off. Eventually these may be
// stabilized and removed.
// Scroll speed multiplier for swiping
const static int kMouseSensitivity = 2.5;
// Input Axis inversion
// 1 for standard, -1 for inverted
const static int kXAxisInversion = -1;
const static int kYAxisInversion = -1;
// Experimental value for bounding the maximum zoom ratio
const static int kMaxZoomSize = 3;
} // namespace
@interface SceneView (Private)
// Returns the number of pixels displayed per device pixel when the scaling is
// such that the entire frame would fit perfectly in content. Note the ratios
// are different for width and height, some people have multiple monitors, some
// have 16:9 or 4:3 while iPad is always single screen, but different iOS
// devices have different resolutions.
- (CGPoint)pixelRatio;
// Return the FrameSize in perspective of the CLIENT resolution
- (webrtc::DesktopSize)frameSizeToScale:(float)scale;
// When bounded on the top and right, this point is where the scene must be
// positioned given a scene size
- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size;
// Converts a point in the the CLIENT resolution to a similar point in the HOST
// resolution. Additionally, CLIENT resolution is expressed in float values
// while HOST operates in integer values.
- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint
targetPoint:(webrtc::DesktopVector&)desktopPoint;
// Converts a point in the the HOST resolution to a similar point in the CLIENT
// resolution. Additionally, CLIENT resolution is expressed in float values
// while HOST operates in integer values.
- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint
targetPoint:(CGPoint&)touchPoint;
@end
@implementation SceneView
- (id)init {
self = [super init];
if (self) {
_frameSize = webrtc::DesktopSize(1, 1);
_contentSize = webrtc::DesktopSize(1, 1);
_mousePosition = webrtc::DesktopVector(0, 0);
_position = GLKVector3Make(0, 0, 1);
_margin.left = 0;
_margin.right = 0;
_margin.top = 0;
_margin.bottom = 0;
_anchored.left = false;
_anchored.right = false;
_anchored.top = false;
_anchored.bottom = false;
}
return self;
}
- (const GLKMatrix4&)projectionMatrix {
return _projectionMatrix;
}
- (const GLKMatrix4&)modelViewMatrix {
// Start by using the entire scene
_modelViewMatrix = GLKMatrix4Identity;
// Position scene according to any panning or bounds
_modelViewMatrix = GLKMatrix4Translate(_modelViewMatrix,
_position.x + _margin.left,
_position.y + _margin.bottom,
0.0);
// Apply zoom
_modelViewMatrix = GLKMatrix4Scale(_modelViewMatrix,
_position.z / self.pixelRatio.x,
_position.z / self.pixelRatio.y,
1.0);
// We are directly above the screen and looking down.
static const GLKMatrix4 viewMatrix = GLKMatrix4MakeLookAt(
0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); // center view
_modelViewMatrix = GLKMatrix4Multiply(viewMatrix, _modelViewMatrix);
return _modelViewMatrix;
}
- (const webrtc::DesktopSize&)contentSize {
return _contentSize;
}
- (void)setContentSize:(const CGSize&)size {
_contentSize.set(size.width, size.height);
_projectionMatrix = GLKMatrix4MakeOrtho(
0.0, _contentSize.width(), 0.0, _contentSize.height(), 1.0, -1.0);
TexturedQuad newQuad;
newQuad.bl.geometryVertex = CGPointMake(0.0, 0.0);
newQuad.br.geometryVertex = CGPointMake(_contentSize.width(), 0.0);
newQuad.tl.geometryVertex = CGPointMake(0.0, _contentSize.height());
newQuad.tr.geometryVertex =
CGPointMake(_contentSize.width(), _contentSize.height());
newQuad.bl.textureVertex = CGPointMake(0.0, 1.0);
newQuad.br.textureVertex = CGPointMake(1.0, 1.0);
newQuad.tl.textureVertex = CGPointMake(0.0, 0.0);
newQuad.tr.textureVertex = CGPointMake(1.0, 0.0);
_glQuad = newQuad;
}
- (const webrtc::DesktopSize&)frameSize {
return _frameSize;
}
- (void)setFrameSize:(const webrtc::DesktopSize&)size {
DCHECK(size.width() > 0 && size.height() > 0);
// Don't do anything if the size has not changed.
if (_frameSize.equals(size))
return;
_frameSize.set(size.width(), size.height());
_position.x = 0;
_position.y = 0;
float verticalPixelScaleRatio =
(static_cast<float>(_contentSize.height() - _margin.top -
_margin.bottom) /
static_cast<float>(_frameSize.height())) /
_position.z;
// Anchored at the position (0,0)
_anchored.left = YES;
_anchored.right = NO;
_anchored.top = NO;
_anchored.bottom = YES;
[self panAndZoom:CGPointMake(0.0, 0.0) scaleBy:verticalPixelScaleRatio];
// Center the mouse on the CLIENT screen
webrtc::DesktopVector centerMouseLocation;
if ([self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2,
_contentSize.height() / 2)
targetPoint:centerMouseLocation]) {
_mousePosition.set(centerMouseLocation.x(), centerMouseLocation.y());
}
#if DEBUG
NSLog(@"resized frame:%d:%d scale:%f",
_frameSize.width(),
_frameSize.height(),
_position.z);
#endif // DEBUG
}
- (const webrtc::DesktopVector&)mousePosition {
return _mousePosition;
}
- (void)setPanVelocity:(const CGPoint&)delta {
_panVelocity.x = delta.x;
_panVelocity.y = delta.y;
}
- (void)setMarginsFromLeft:(int)left
right:(int)right
top:(int)top
bottom:(int)bottom {
_margin.left = left;
_margin.right = right;
_margin.top = top;
_margin.bottom = bottom;
}
- (void)draw {
glEnableVertexAttribArray(GLKVertexAttribPosition);
glEnableVertexAttribArray(GLKVertexAttribTexCoord0);
glEnableVertexAttribArray(GLKVertexAttribTexCoord1);
// Define our scene space
glVertexAttribPointer(GLKVertexAttribPosition,
2,
GL_FLOAT,
GL_FALSE,
sizeof(TexturedVertex),
&(_glQuad.bl.geometryVertex));
// Define the desktop plane
glVertexAttribPointer(GLKVertexAttribTexCoord0,
2,
GL_FLOAT,
GL_FALSE,
sizeof(TexturedVertex),
&(_glQuad.bl.textureVertex));
// Define the cursor plane
glVertexAttribPointer(GLKVertexAttribTexCoord1,
2,
GL_FLOAT,
GL_FALSE,
sizeof(TexturedVertex),
&(_glQuad.bl.textureVertex));
// Draw!
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
[Utility logGLErrorCode:@"SceneView draw"];
}
- (CGPoint)pixelRatio {
CGPoint r = CGPointMake(static_cast<float>(_contentSize.width()) /
static_cast<float>(_frameSize.width()),
static_cast<float>(_contentSize.height()) /
static_cast<float>(_frameSize.height()));
return r;
}
- (webrtc::DesktopSize)frameSizeToScale:(float)scale {
return webrtc::DesktopSize(_frameSize.width() * scale,
_frameSize.height() * scale);
}
- (webrtc::DesktopVector)getBoundsForSize:(const webrtc::DesktopSize&)size {
webrtc::DesktopVector r(
_contentSize.width() - _margin.left - _margin.right - size.width(),
_contentSize.height() - _margin.bottom - _margin.top - size.height());
if (r.x() > 0) {
r.set((_contentSize.width() - size.width()) / 2, r.y());
}
if (r.y() > 0) {
r.set(r.x(), (_contentSize.height() - size.height()) / 2);
}
return r;
}
- (BOOL)containsTouchPoint:(CGPoint)point {
// Here frame is from the top-left corner, most other calculations are framed
// from the bottom left.
CGRect frame =
CGRectMake(_margin.left,
_margin.top,
_contentSize.width() - _margin.left - _margin.right,
_contentSize.height() - _margin.top - _margin.bottom);
return CGRectContainsPoint(frame, point);
}
- (BOOL)convertTouchPointToMousePoint:(CGPoint)touchPoint
targetPoint:(webrtc::DesktopVector&)mousePoint {
if (![self containsTouchPoint:touchPoint]) {
return NO;
}
// A touch location occurs in respect to the user's entire view surface.
// The GL Context is upside down from the User's perspective so flip it.
CGPoint glOrientedTouchPoint =
CGPointMake(touchPoint.x, _contentSize.height() - touchPoint.y);
// The GL surface generally is not at the same origination point as the touch,
// so translate by the scene's position.
CGPoint glOrientedPointInRespectToFrame =
CGPointMake(glOrientedTouchPoint.x - _position.x,
glOrientedTouchPoint.y - _position.y);
// The perspective exists in relative to the CLIENT resolution at 1:1, zoom
// our perspective so we are relative to the HOST at 1:1
CGPoint glOrientedPointInFrame =
CGPointMake(glOrientedPointInRespectToFrame.x / _position.z,
glOrientedPointInRespectToFrame.y / _position.z);
// Finally, flip the perspective back over to the Users, but this time in
// respect to the HOST desktop. Floor to ensure the result is always in
// frame.
CGPoint deskTopOrientedPointInFrame =
CGPointMake(floorf(glOrientedPointInFrame.x),
floorf(_frameSize.height() - glOrientedPointInFrame.y));
// Convert from float to integer
mousePoint.set(deskTopOrientedPointInFrame.x, deskTopOrientedPointInFrame.y);
return CGRectContainsPoint(
CGRectMake(0, 0, _frameSize.width(), _frameSize.height()),
deskTopOrientedPointInFrame);
}
- (BOOL)convertMousePointToTouchPoint:(const webrtc::DesktopVector&)mousePoint
targetPoint:(CGPoint&)touchPoint {
// A mouse point is in respect to the desktop frame.
// Flip the perspective back over to the Users, in
// respect to the HOST desktop.
CGPoint deskTopOrientedPointInFrame =
CGPointMake(mousePoint.x(), _frameSize.height() - mousePoint.y());
// The perspective exists in relative to the CLIENT resolution at 1:1, zoom
// our perspective so we are relative to the HOST at 1:1
CGPoint glOrientedPointInFrame =
CGPointMake(deskTopOrientedPointInFrame.x * _position.z,
deskTopOrientedPointInFrame.y * _position.z);
// The GL surface generally is not at the same origination point as the touch,
// so translate by the scene's position.
CGPoint glOrientedPointInRespectToFrame =
CGPointMake(glOrientedPointInFrame.x + _position.x,
glOrientedPointInFrame.y + _position.y);
// Convert from float to integer
touchPoint.x = floorf(glOrientedPointInRespectToFrame.x);
touchPoint.y = floorf(glOrientedPointInRespectToFrame.y);
return [self containsTouchPoint:touchPoint];
}
- (void)panAndZoom:(CGPoint)translation scaleBy:(float)ratio {
CGPoint ratios = [self pixelRatio];
// New Scaling factor bounded by a min and max
float resultScale = _position.z * ratio;
float scaleUpperBound = MAX(ratios.x, MAX(ratios.y, kMaxZoomSize));
float scaleLowerBound = MIN(ratios.x, ratios.y);
if (resultScale < scaleLowerBound) {
resultScale = scaleLowerBound;
} else if (resultScale > scaleUpperBound) {
resultScale = scaleUpperBound;
}
DCHECK(isnormal(resultScale) && resultScale > 0);
// The GL perspective is upside down in relation to the User's view, so flip
// the translation
translation.y = -translation.y;
// The constants here could be user options later.
translation.x =
translation.x * kXAxisInversion * (1 / (ratios.x * kMouseSensitivity));
translation.y =
translation.y * kYAxisInversion * (1 / (ratios.y * kMouseSensitivity));
CGPoint delta = CGPointMake(0, 0);
CGPoint scaleDelta = CGPointMake(0, 0);
webrtc::DesktopSize currentSize = [self frameSizeToScale:_position.z];
{
// Closure for this variable, so the variable is not available to the rest
// of this function
webrtc::DesktopVector currentBounds = [self getBoundsForSize:currentSize];
// There are rounding errors in the scope of this function, see the
// butterfly effect. In successive calls, the resulting position isn't
// always exactly the calculated position. If we know we are Anchored, then
// go ahead and reposition it to the values above.
if (_anchored.right) {
_position.x = currentBounds.x();
}
if (_anchored.top) {
_position.y = currentBounds.y();
}
}
if (_position.z != resultScale) {
// When scaling the scene, the origination of scaling is the mouse's
// location. But when the frame is anchored, adjust the origination to the
// anchor point.
CGPoint mousePositionInClientResolution;
[self convertMousePointToTouchPoint:_mousePosition
targetPoint:mousePositionInClientResolution];
// Prefer to zoom based on the left anchor when there is a choice
if (_anchored.left) {
mousePositionInClientResolution.x = 0;
} else if (_anchored.right) {
mousePositionInClientResolution.x = _contentSize.width();
}
// Prefer to zoom out from the top anchor when there is a choice
if (_anchored.top) {
mousePositionInClientResolution.y = _contentSize.height();
} else if (_anchored.bottom) {
mousePositionInClientResolution.y = 0;
}
scaleDelta.x -=
[SceneView positionDeltaFromScaling:ratio
position:_position.x
length:currentSize.width()
anchor:mousePositionInClientResolution.x];
scaleDelta.y -=
[SceneView positionDeltaFromScaling:ratio
position:_position.y
length:currentSize.height()
anchor:mousePositionInClientResolution.y];
}
delta.x = [SceneView
positionDeltaFromTranslation:translation.x
position:_position.x
freeSpace:_contentSize.width() - currentSize.width()
scaleingPositionDelta:scaleDelta.x
isAnchoredLow:_anchored.left
isAnchoredHigh:_anchored.right];
delta.y = [SceneView
positionDeltaFromTranslation:translation.y
position:_position.y
freeSpace:_contentSize.height() - currentSize.height()
scaleingPositionDelta:scaleDelta.y
isAnchoredLow:_anchored.bottom
isAnchoredHigh:_anchored.top];
{
// Closure for this variable, so the variable is not available to the rest
// of this function
webrtc::DesktopVector bounds =
[self getBoundsForSize:[self frameSizeToScale:resultScale]];
delta.x = [SceneView boundDeltaFromPosition:_position.x
delta:delta.x
lowerBound:bounds.x()
upperBound:0];
delta.y = [SceneView boundDeltaFromPosition:_position.y
delta:delta.y
lowerBound:bounds.y()
upperBound:0];
}
BOOL isLeftAndRightAnchored = _anchored.left && _anchored.right;
BOOL isTopAndBottomAnchored = _anchored.top && _anchored.bottom;
[self updateMousePositionAndAnchorsWithTranslation:translation
scale:resultScale];
// If both anchors were lost, then keep the one that is easier to predict
if (isLeftAndRightAnchored && !_anchored.left && !_anchored.right) {
delta.x = -_position.x;
_anchored.left = YES;
}
// If both anchors were lost, then keep the one that is easier to predict
if (isTopAndBottomAnchored && !_anchored.top && !_anchored.bottom) {
delta.y = -_position.y;
_anchored.bottom = YES;
}
// FINALLY, update the scene's position
_position.x += delta.x;
_position.y += delta.y;
_position.z = resultScale;
}
- (void)updateMousePositionAndAnchorsWithTranslation:(CGPoint)translation
scale:(float)scale {
webrtc::DesktopVector centerMouseLocation;
[self convertTouchPointToMousePoint:CGPointMake(_contentSize.width() / 2,
_contentSize.height() / 2)
targetPoint:centerMouseLocation];
webrtc::DesktopVector currentBounds =
[self getBoundsForSize:[self frameSizeToScale:_position.z]];
webrtc::DesktopVector nextBounds =
[self getBoundsForSize:[self frameSizeToScale:scale]];
webrtc::DesktopVector predictedMousePosition(
_mousePosition.x() - translation.x, _mousePosition.y() + translation.y);
_mousePosition.set(
[SceneView boundMouseGivenNextPosition:predictedMousePosition.x()
maxPosition:_frameSize.width()
centerPosition:centerMouseLocation.x()
isAnchoredLow:_anchored.left
isAnchoredHigh:_anchored.right],
[SceneView boundMouseGivenNextPosition:predictedMousePosition.y()
maxPosition:_frameSize.height()
centerPosition:centerMouseLocation.y()
isAnchoredLow:_anchored.top
isAnchoredHigh:_anchored.bottom]);
_panVelocity.x = [SceneView boundVelocity:_panVelocity.x
axisLength:_frameSize.width()
mousePosition:_mousePosition.x()];
_panVelocity.y = [SceneView boundVelocity:_panVelocity.y
axisLength:_frameSize.height()
mousePosition:_mousePosition.y()];
_anchored.left = (nextBounds.x() >= 0) ||
(_position.x == 0 &&
predictedMousePosition.x() <= centerMouseLocation.x());
_anchored.right =
(nextBounds.x() >= 0) ||
(_position.x == currentBounds.x() &&
predictedMousePosition.x() >= centerMouseLocation.x()) ||
(_mousePosition.x() == _frameSize.width() - 1 && !_anchored.left);
_anchored.bottom = (nextBounds.y() >= 0) ||
(_position.y == 0 &&
predictedMousePosition.y() >= centerMouseLocation.y());
_anchored.top =
(nextBounds.y() >= 0) ||
(_position.y == currentBounds.y() &&
predictedMousePosition.y() <= centerMouseLocation.y()) ||
(_mousePosition.y() == _frameSize.height() - 1 && !_anchored.bottom);
}
+ (float)positionDeltaFromScaling:(float)ratio
position:(float)position
length:(float)length
anchor:(float)anchor {
float newSize = length * ratio;
float scaleXBy = fabs(position - anchor) / length;
float delta = (newSize - length) * scaleXBy;
return delta;
}
+ (int)positionDeltaFromTranslation:(int)translation
position:(int)position
freeSpace:(int)freeSpace
scaleingPositionDelta:(int)scaleingPositionDelta
isAnchoredLow:(BOOL)isAnchoredLow
isAnchoredHigh:(BOOL)isAnchoredHigh {
if (isAnchoredLow && isAnchoredHigh) {
// center the view
return (freeSpace / 2) - position;
} else if (isAnchoredLow) {
return 0;
} else if (isAnchoredHigh) {
return scaleingPositionDelta;
} else {
return translation + scaleingPositionDelta;
}
}
+ (int)boundDeltaFromPosition:(float)position
delta:(int)delta
lowerBound:(int)lowerBound
upperBound:(int)upperBound {
int result = position + delta;
if (lowerBound < upperBound) { // the view is larger than the bounds
if (result > upperBound) {
result = upperBound;
} else if (result < lowerBound) {
result = lowerBound;
}
} else {
// the view is smaller than the bounds so we'll always be at the lowerBound
result = lowerBound;
}
return result - position;
}
+ (int)boundMouseGivenNextPosition:(int)nextPosition
maxPosition:(int)maxPosition
centerPosition:(int)centerPosition
isAnchoredLow:(BOOL)isAnchoredLow
isAnchoredHigh:(BOOL)isAnchoredHigh {
if (nextPosition < 0) {
return 0;
}
if (nextPosition > maxPosition - 1) {
return maxPosition - 1;
}
if ((isAnchoredLow && nextPosition <= centerPosition) ||
(isAnchoredHigh && nextPosition >= centerPosition)) {
return nextPosition;
}
return centerPosition;
}
+ (float)boundVelocity:(float)velocity
axisLength:(int)axisLength
mousePosition:(int)mousePosition {
if (velocity != 0) {
if (mousePosition <= 0 || mousePosition >= (axisLength - 1)) {
return 0;
}
}
return velocity;
}
- (BOOL)tickPanVelocity {
BOOL inMotion = ((_panVelocity.x != 0.0) || (_panVelocity.y != 0.0));
if (inMotion) {
uint32_t divisor = 50 / _position.z;
float reducer = .95;
if (_panVelocity.x != 0.0 && ABS(_panVelocity.x) < divisor) {
_panVelocity = CGPointMake(0.0, _panVelocity.y);
}
if (_panVelocity.y != 0.0 && ABS(_panVelocity.y) < divisor) {
_panVelocity = CGPointMake(_panVelocity.x, 0.0);
}
[self panAndZoom:CGPointMake(_panVelocity.x / divisor,
_panVelocity.y / divisor)
scaleBy:1.0];
_panVelocity.x *= reducer;
_panVelocity.y *= reducer;
}
return inMotion;
}
@end