1 /**
  2  * Copyright (C) 2010-2014 KO GmbH <copyright@kogmbh.com>
  3  *
  4  * @licstart
  5  * This file is part of WebODF.
  6  *
  7  * WebODF is free software: you can redistribute it and/or modify it
  8  * under the terms of the GNU Affero General Public License (GNU AGPL)
  9  * as published by the Free Software Foundation, either version 3 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * WebODF is distributed in the hope that it will be useful, but
 13  * WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU Affero General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Affero General Public License
 18  * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 19  * @licend
 20  *
 21  * @source: http://www.webodf.org/
 22  * @source: https://github.com/kogmbh/WebODF/
 23  */
 24 
 25 /*global gui*/
 26 
 27 /**
 28  * Finds line-wrap points by comparing the visual overlap between visible rectangles.
 29  *
 30  * @constructor
 31  * @implements {gui.VisualStepScanner}
 32  */
 33 gui.LineBoundaryScanner = function () {
 34     "use strict";
 35     var self = this,
 36         lineRect = null,
 37         // Minimum amount of overlap between two rectangles to be considered on the same line is arbitrarily 40%
 38         /**@const*/ MIN_OVERLAP_THRESHOLD = 0.4;
 39 
 40     /**
 41      * Return the fraction of overlap between two rectangles. If there is
 42      * no overlap, or either of the rectangles is 0 height, this will
 43      * return 0.
 44      *
 45      * @param {!core.SimpleClientRect} rect1
 46      * @param {!core.SimpleClientRect} rect2
 47      * @return {!number}
 48      */
 49     function verticalOverlapPercent(rect1, rect2) {
 50         var rect1Height = rect1.bottom - rect1.top,
 51             rect2Height = rect2.bottom - rect2.top,
 52             minRectHeight = Math.min(rect1Height, rect2Height),
 53             intersectTop = Math.max(rect1.top, rect2.top),
 54             intersectBottom = Math.min(rect1.bottom, rect2.bottom),
 55             overlapHeight = intersectBottom - intersectTop;
 56 
 57         return minRectHeight > 0 ? (overlapHeight / minRectHeight) : 0;
 58     }
 59 
 60     /**
 61      * Returns true if the amount of overlap between the known line rectangle and the visible next rectangle
 62      * is below the specified MIN_OVERLAP_THRESHOLD. If there is no known line rectangle, this will return false.
 63      *
 64      * @param {!core.SimpleClientRect} nextRect Client rect of next step (by direction)
 65      * @return {!boolean}
 66      */
 67     function isLineBoundary(nextRect) {
 68         if (lineRect) {
 69             // TODO this logic will fail if the caret is between a subscript & superscript char as the overlap will be 0
 70             return verticalOverlapPercent(/**@type{!core.SimpleClientRect}*/(lineRect), nextRect) <= MIN_OVERLAP_THRESHOLD;
 71         }
 72         return false;
 73     }
 74 
 75     /**
 76      * @param {!core.SimpleClientRect} rect1
 77      * @param {!core.SimpleClientRect} rect2
 78      * @return {!core.SimpleClientRect}
 79      */
 80     function combineRects(rect1, rect2) {
 81         return {
 82             left: Math.min(rect1.left, rect2.left),
 83             right: Math.max(rect1.right, rect2.right),
 84             top: Math.min(rect1.top, rect2.top),
 85             bottom: Math.min(rect1.bottom, rect2.bottom)
 86         };
 87     }
 88 
 89     /**
 90      * @param {?core.SimpleClientRect} originalRect
 91      * @param {?core.SimpleClientRect} newRect
 92      * @return {?core.SimpleClientRect}
 93      */
 94     function growRect(originalRect, newRect) {
 95         if (originalRect && newRect) {
 96             return combineRects(/**@type{!core.SimpleClientRect}*/(originalRect),
 97                                 /**@type{!core.SimpleClientRect}*/(newRect));
 98         }
 99         return originalRect || newRect;
100     }
101 
102     this.token = undefined;
103 
104     /**
105      * @param {!gui.StepInfo} stepInfo
106      * @param {?ClientRect} previousRect
107      * @param {?ClientRect} nextRect
108      * @return {!boolean}
109      */
110     this.process = function(stepInfo, previousRect, nextRect) {
111         // Can only detect line boundaries when the next rectangle is visible. An invisible next-rect
112         // indicates the next step does not have any visible content attached, so it's location on screen
113         // is impossible to determine accurately.
114         var isOverLineBoundary = nextRect && isLineBoundary(/**@type{!core.SimpleClientRect}*/(nextRect));
115 
116         if (previousRect && (!nextRect || isOverLineBoundary)) {
117             // Detect a possible line wrap point in one of two ways:
118             // 1. Going from a visible to an invisible rectangle. An invisible rectangle can indicate a collapsed
119             //      whitespace text node, or an invisible element that the browser may choose to wrap at.
120             // 2. A confirmed wrap point where the nextRect is visible and clearly not on the same line as the previous.
121             self.token = stepInfo.token;
122         }
123 
124         if (isOverLineBoundary) {
125             return true;
126         }
127 
128         // Grow the current line rectangle by the (now approved) previous rectangle. This allows the line height
129         // to grow naturally.
130         lineRect = growRect(lineRect, previousRect);
131         return false;
132     };
133 };