blob: 9984b263efff43d017cd7e99aeb440cd876d7940 [file] [log] [blame]
// Copyright 2015 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.
/** @fileoverview Logic for panning a braille display within a line of braille
* content that might not fit on a single display.
*/
goog.provide('cvox.PanStrategy');
/**
* @constructor
*
* A stateful class that keeps track of the current 'viewport' of a braille
* display in a line of content.
*/
cvox.PanStrategy = function() {
/**
* @type {number}
* @private
*/
this.displaySize_ = 0;
/**
* @type {number}
* @private
*/
this.contentLength_ = 0;
/**
* Points before which it is desirable to break content if it doesn't fit
* on the display.
* @type {!Array<number>}
* @private
*/
this.breakPoints_ = [];
/**
* @type {!cvox.PanStrategy.Range}
* @private
*/
this.viewPort_ = {start: 0, end: 0};
};
/**
* A range used to represent the viewport with inclusive start and xclusive
* end position.
* @typedef {{start: number, end: number}}
*/
cvox.PanStrategy.Range;
cvox.PanStrategy.prototype = {
/**
* Gets the current viewport which is never larger than the current
* display size and whose end points are always within the limits of
* the current content.
* @type {!cvox.PanStrategy.Range}
*/
get viewPort() {
return this.viewPort_;
},
/**
* Sets the display size. This call may update the viewport.
* @param {number} size the new display size, or {@code 0} if no display is
* present.
*/
setDisplaySize: function(size) {
this.displaySize_ = size;
this.panToPosition_(this.viewPort_.start);
},
/**
* Sets the current content that panning should happen within. This call may
* change the viewport.
* @param {!ArrayBuffer} translatedContent The new content.
* @param {number} targetPosition Target position. The viewport is changed
* to overlap this position.
*/
setContent: function(translatedContent, targetPosition) {
this.breakPoints_ = this.calculateBreakPoints_(translatedContent);
this.contentLength_ = translatedContent.byteLength;
this.panToPosition_(targetPosition);
},
/**
* If possible, changes the viewport to a part of the line that follows
* the current viewport.
* @return {boolean} {@code true} if the viewport was changed.
*/
next: function() {
var newStart = this.viewPort_.end;
var newEnd;
if (newStart + this.displaySize_ < this.contentLength_) {
newEnd = this.extendRight_(newStart);
} else {
newEnd = this.contentLength_;
}
if (newEnd > newStart) {
this.viewPort_ = {start: newStart, end: newEnd};
return true;
}
return false;
},
/**
* If possible, changes the viewport to a part of the line that precedes
* the current viewport.
* @return {boolean} {@code true} if the viewport was changed.
*/
previous: function() {
if (this.viewPort_.start > 0) {
var newStart, newEnd;
if (this.viewPort_.start <= this.displaySize_) {
newStart = 0;
newEnd = this.extendRight_(newStart);
} else {
newEnd = this.viewPort_.start;
var limit = newEnd - this.displaySize_;
newStart = limit;
var pos = 0;
while (pos < this.breakPoints_.length &&
this.breakPoints_[pos] < limit) {
pos++;
}
if (pos < this.breakPoints_.length &&
this.breakPoints_[pos] < newEnd) {
newStart = this.breakPoints_[pos];
}
}
if (newStart < newEnd) {
this.viewPort_ = {start: newStart, end: newEnd};
return true;
}
}
return false;
},
/**
* Finds the end position for a new viewport start position, considering
* current breakpoints as well as display size and content length.
* @param {number} from Start of the region to extend.
* @return {number}
* @private
*/
extendRight_: function(from) {
var limit = Math.min(from + this.displaySize_, this.contentLength_);
var pos = 0;
var result = limit;
while (pos < this.breakPoints_.length && this.breakPoints_[pos] <= from) {
pos++;
}
while (pos < this.breakPoints_.length && this.breakPoints_[pos] <= limit) {
result = this.breakPoints_[pos];
pos++;
}
return result;
},
/**
* Overridden by subclasses to provide breakpoints given translated
* braille cell content.
* @param {!ArrayBuffer} content New display content.
* @return {!Array<number>} The points before which it is desirable to break
* content if needed or the empty array if no points are more desirable
* than any position.
* @private
*/
calculateBreakPoints_: function(content) {return [];},
/**
* Moves the viewport so that it overlaps a target position without taking
* the current viewport position into consideration.
* @param {number} position Target position.
*/
panToPosition_: function(position) {
if (this.displaySize_ > 0) {
this.viewPort_ = {start: 0, end: 0};
while (this.next() && this.viewPort_.end <= position) {
// Nothing to do.
}
} else {
this.viewPort_ = {start: position, end: position};
}
},
};
/**
* A pan strategy that fits as much content on the display as possible, that
* is, it doesn't do any wrapping.
* @constructor
* @extends {cvox.PanStrategy}
*/
cvox.FixedPanStrategy = cvox.PanStrategy;
/**
* A pan strategy that tries to wrap 'words' when breaking content.
* A 'word' in this context is just a chunk of non-blank braille cells
* delimited by blank cells.
* @constructor
* @extends {cvox.PanStrategy}
*/
cvox.WrappingPanStrategy = function() {
cvox.PanStrategy.call(this);
};
cvox.WrappingPanStrategy.prototype = {
__proto__: cvox.PanStrategy.prototype,
/** @override */
calculateBreakPoints_: function(content) {
var view = new Uint8Array(content);
var newContentLength = view.length;
var result = [];
var lastCellWasBlank = false;
for (var pos = 0; pos < view.length; ++pos) {
if (lastCellWasBlank && view[pos] != 0) {
result.push(pos);
}
lastCellWasBlank = (view[pos] == 0);
}
return result;
},
};