position_measurement.min.js (28707B)
1 import { buildLineContent, LineView } from "../line/line_data.js" 2 import { clipPos, Pos } from "../line/pos.js" 3 import { collapsedSpanAtEnd, heightAtLine, lineIsHidden, visualLine } from "../line/spans.js" 4 import { getLine, lineAtHeight, lineNo, updateLineHeight } from "../line/utils_line.js" 5 import { bidiOther, getBidiPartAt, getOrder } from "../util/bidi.js" 6 import { chrome, android, ie, ie_version } from "../util/browser.js" 7 import { elt, removeChildren, range, removeChildrenAndAdd } from "../util/dom.js" 8 import { e_target } from "../util/event.js" 9 import { hasBadZoomedRects } from "../util/feature_detection.js" 10 import { countColumn, findFirst, isExtendingChar, scrollerGap, skipExtendingChars } from "../util/misc.js" 11 import { updateLineForChanges } from "../display/update_line.js" 12 13 import { widgetHeight } from "./widgets.js" 14 15 // POSITION MEASUREMENT 16 17 export function paddingTop(display) {return display.lineSpace.offsetTop} 18 export function paddingVert(display) {return display.mover.offsetHeight - display.lineSpace.offsetHeight} 19 export function paddingH(display) { 20 if (display.cachedPaddingH) return display.cachedPaddingH 21 let e = removeChildrenAndAdd(display.measure, elt("pre", "x")) 22 let style = window.getComputedStyle ? window.getComputedStyle(e) : e.currentStyle 23 let data = {left: parseInt(style.paddingLeft), right: parseInt(style.paddingRight)} 24 if (!isNaN(data.left) && !isNaN(data.right)) display.cachedPaddingH = data 25 return data 26 } 27 28 export function scrollGap(cm) { return scrollerGap - cm.display.nativeBarWidth } 29 export function displayWidth(cm) { 30 return cm.display.scroller.clientWidth - scrollGap(cm) - cm.display.barWidth 31 } 32 export function displayHeight(cm) { 33 return cm.display.scroller.clientHeight - scrollGap(cm) - cm.display.barHeight 34 } 35 36 // Ensure the lineView.wrapping.heights array is populated. This is 37 // an array of bottom offsets for the lines that make up a drawn 38 // line. When lineWrapping is on, there might be more than one 39 // height. 40 function ensureLineHeights(cm, lineView, rect) { 41 let wrapping = cm.options.lineWrapping 42 let curWidth = wrapping && displayWidth(cm) 43 if (!lineView.measure.heights || wrapping && lineView.measure.width != curWidth) { 44 let heights = lineView.measure.heights = [] 45 if (wrapping) { 46 lineView.measure.width = curWidth 47 let rects = lineView.text.firstChild.getClientRects() 48 for (let i = 0; i < rects.length - 1; i++) { 49 let cur = rects[i], next = rects[i + 1] 50 if (Math.abs(cur.bottom - next.bottom) > 2) 51 heights.push((cur.bottom + next.top) / 2 - rect.top) 52 } 53 } 54 heights.push(rect.bottom - rect.top) 55 } 56 } 57 58 // Find a line map (mapping character offsets to text nodes) and a 59 // measurement cache for the given line number. (A line view might 60 // contain multiple lines when collapsed ranges are present.) 61 export function mapFromLineView(lineView, line, lineN) { 62 if (lineView.line == line) 63 return {map: lineView.measure.map, cache: lineView.measure.cache} 64 for (let i = 0; i < lineView.rest.length; i++) 65 if (lineView.rest[i] == line) 66 return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i]} 67 for (let i = 0; i < lineView.rest.length; i++) 68 if (lineNo(lineView.rest[i]) > lineN) 69 return {map: lineView.measure.maps[i], cache: lineView.measure.caches[i], before: true} 70 } 71 72 // Render a line into the hidden node display.externalMeasured. Used 73 // when measurement is needed for a line that's not in the viewport. 74 function updateExternalMeasurement(cm, line) { 75 line = visualLine(line) 76 let lineN = lineNo(line) 77 let view = cm.display.externalMeasured = new LineView(cm.doc, line, lineN) 78 view.lineN = lineN 79 let built = view.built = buildLineContent(cm, view) 80 view.text = built.pre 81 removeChildrenAndAdd(cm.display.lineMeasure, built.pre) 82 return view 83 } 84 85 // Get a {top, bottom, left, right} box (in line-local coordinates) 86 // for a given character. 87 export function measureChar(cm, line, ch, bias) { 88 return measureCharPrepared(cm, prepareMeasureForLine(cm, line), ch, bias) 89 } 90 91 // Find a line view that corresponds to the given line number. 92 export function findViewForLine(cm, lineN) { 93 if (lineN >= cm.display.viewFrom && lineN < cm.display.viewTo) 94 return cm.display.view[findViewIndex(cm, lineN)] 95 let ext = cm.display.externalMeasured 96 if (ext && lineN >= ext.lineN && lineN < ext.lineN + ext.size) 97 return ext 98 } 99 100 // Measurement can be split in two steps, the set-up work that 101 // applies to the whole line, and the measurement of the actual 102 // character. Functions like coordsChar, that need to do a lot of 103 // measurements in a row, can thus ensure that the set-up work is 104 // only done once. 105 export function prepareMeasureForLine(cm, line) { 106 let lineN = lineNo(line) 107 let view = findViewForLine(cm, lineN) 108 if (view && !view.text) { 109 view = null 110 } else if (view && view.changes) { 111 updateLineForChanges(cm, view, lineN, getDimensions(cm)) 112 cm.curOp.forceUpdate = true 113 } 114 if (!view) 115 view = updateExternalMeasurement(cm, line) 116 117 let info = mapFromLineView(view, line, lineN) 118 return { 119 line: line, view: view, rect: null, 120 map: info.map, cache: info.cache, before: info.before, 121 hasHeights: false 122 } 123 } 124 125 // Given a prepared measurement object, measures the position of an 126 // actual character (or fetches it from the cache). 127 export function measureCharPrepared(cm, prepared, ch, bias, varHeight) { 128 if (prepared.before) ch = -1 129 let key = ch + (bias || ""), found 130 if (prepared.cache.hasOwnProperty(key)) { 131 found = prepared.cache[key] 132 } else { 133 if (!prepared.rect) 134 prepared.rect = prepared.view.text.getBoundingClientRect() 135 if (!prepared.hasHeights) { 136 ensureLineHeights(cm, prepared.view, prepared.rect) 137 prepared.hasHeights = true 138 } 139 found = measureCharInner(cm, prepared, ch, bias) 140 if (!found.bogus) prepared.cache[key] = found 141 } 142 return {left: found.left, right: found.right, 143 top: varHeight ? found.rtop : found.top, 144 bottom: varHeight ? found.rbottom : found.bottom} 145 } 146 147 let nullRect = {left: 0, right: 0, top: 0, bottom: 0} 148 149 export function nodeAndOffsetInLineMap(map, ch, bias) { 150 let node, start, end, collapse, mStart, mEnd 151 // First, search the line map for the text node corresponding to, 152 // or closest to, the target character. 153 for (let i = 0; i < map.length; i += 3) { 154 mStart = map[i] 155 mEnd = map[i + 1] 156 if (ch < mStart) { 157 start = 0; end = 1 158 collapse = "left" 159 } else if (ch < mEnd) { 160 start = ch - mStart 161 end = start + 1 162 } else if (i == map.length - 3 || ch == mEnd && map[i + 3] > ch) { 163 end = mEnd - mStart 164 start = end - 1 165 if (ch >= mEnd) collapse = "right" 166 } 167 if (start != null) { 168 node = map[i + 2] 169 if (mStart == mEnd && bias == (node.insertLeft ? "left" : "right")) 170 collapse = bias 171 if (bias == "left" && start == 0) 172 while (i && map[i - 2] == map[i - 3] && map[i - 1].insertLeft) { 173 node = map[(i -= 3) + 2] 174 collapse = "left" 175 } 176 if (bias == "right" && start == mEnd - mStart) 177 while (i < map.length - 3 && map[i + 3] == map[i + 4] && !map[i + 5].insertLeft) { 178 node = map[(i += 3) + 2] 179 collapse = "right" 180 } 181 break 182 } 183 } 184 return {node: node, start: start, end: end, collapse: collapse, coverStart: mStart, coverEnd: mEnd} 185 } 186 187 function getUsefulRect(rects, bias) { 188 let rect = nullRect 189 if (bias == "left") for (let i = 0; i < rects.length; i++) { 190 if ((rect = rects[i]).left != rect.right) break 191 } else for (let i = rects.length - 1; i >= 0; i--) { 192 if ((rect = rects[i]).left != rect.right) break 193 } 194 return rect 195 } 196 197 function measureCharInner(cm, prepared, ch, bias) { 198 let place = nodeAndOffsetInLineMap(prepared.map, ch, bias) 199 let node = place.node, start = place.start, end = place.end, collapse = place.collapse 200 201 let rect 202 if (node.nodeType == 3) { // If it is a text node, use a range to retrieve the coordinates. 203 for (let i = 0; i < 4; i++) { // Retry a maximum of 4 times when nonsense rectangles are returned 204 while (start && isExtendingChar(prepared.line.text.charAt(place.coverStart + start))) --start 205 while (place.coverStart + end < place.coverEnd && isExtendingChar(prepared.line.text.charAt(place.coverStart + end))) ++end 206 if (ie && ie_version < 9 && start == 0 && end == place.coverEnd - place.coverStart) 207 rect = node.parentNode.getBoundingClientRect() 208 else 209 rect = getUsefulRect(range(node, start, end).getClientRects(), bias) 210 if (rect.left || rect.right || start == 0) break 211 end = start 212 start = start - 1 213 collapse = "right" 214 } 215 if (ie && ie_version < 11) rect = maybeUpdateRectForZooming(cm.display.measure, rect) 216 } else { // If it is a widget, simply get the box for the whole widget. 217 if (start > 0) collapse = bias = "right" 218 let rects 219 if (cm.options.lineWrapping && (rects = node.getClientRects()).length > 1) 220 rect = rects[bias == "right" ? rects.length - 1 : 0] 221 else 222 rect = node.getBoundingClientRect() 223 } 224 if (ie && ie_version < 9 && !start && (!rect || !rect.left && !rect.right)) { 225 let rSpan = node.parentNode.getClientRects()[0] 226 if (rSpan) 227 rect = {left: rSpan.left, right: rSpan.left + charWidth(cm.display), top: rSpan.top, bottom: rSpan.bottom} 228 else 229 rect = nullRect 230 } 231 232 let rtop = rect.top - prepared.rect.top, rbot = rect.bottom - prepared.rect.top 233 let mid = (rtop + rbot) / 2 234 let heights = prepared.view.measure.heights 235 let i = 0 236 for (; i < heights.length - 1; i++) 237 if (mid < heights[i]) break 238 let top = i ? heights[i - 1] : 0, bot = heights[i] 239 let result = {left: (collapse == "right" ? rect.right : rect.left) - prepared.rect.left, 240 right: (collapse == "left" ? rect.left : rect.right) - prepared.rect.left, 241 top: top, bottom: bot} 242 if (!rect.left && !rect.right) result.bogus = true 243 if (!cm.options.singleCursorHeightPerLine) { result.rtop = rtop; result.rbottom = rbot } 244 245 return result 246 } 247 248 // Work around problem with bounding client rects on ranges being 249 // returned incorrectly when zoomed on IE10 and below. 250 function maybeUpdateRectForZooming(measure, rect) { 251 if (!window.screen || screen.logicalXDPI == null || 252 screen.logicalXDPI == screen.deviceXDPI || !hasBadZoomedRects(measure)) 253 return rect 254 let scaleX = screen.logicalXDPI / screen.deviceXDPI 255 let scaleY = screen.logicalYDPI / screen.deviceYDPI 256 return {left: rect.left * scaleX, right: rect.right * scaleX, 257 top: rect.top * scaleY, bottom: rect.bottom * scaleY} 258 } 259 260 export function clearLineMeasurementCacheFor(lineView) { 261 if (lineView.measure) { 262 lineView.measure.cache = {} 263 lineView.measure.heights = null 264 if (lineView.rest) for (let i = 0; i < lineView.rest.length; i++) 265 lineView.measure.caches[i] = {} 266 } 267 } 268 269 export function clearLineMeasurementCache(cm) { 270 cm.display.externalMeasure = null 271 removeChildren(cm.display.lineMeasure) 272 for (let i = 0; i < cm.display.view.length; i++) 273 clearLineMeasurementCacheFor(cm.display.view[i]) 274 } 275 276 export function clearCaches(cm) { 277 clearLineMeasurementCache(cm) 278 cm.display.cachedCharWidth = cm.display.cachedTextHeight = cm.display.cachedPaddingH = null 279 if (!cm.options.lineWrapping) cm.display.maxLineChanged = true 280 cm.display.lineNumChars = null 281 } 282 283 function pageScrollX() { 284 // Work around https://bugs.chromium.org/p/chromium/issues/detail?id=489206 285 // which causes page_Offset and bounding client rects to use 286 // different reference viewports and invalidate our calculations. 287 if (chrome && android) return -(document.body.getBoundingClientRect().left - parseInt(getComputedStyle(document.body).marginLeft)) 288 return window.pageXOffset || (document.documentElement || document.body).scrollLeft 289 } 290 function pageScrollY() { 291 if (chrome && android) return -(document.body.getBoundingClientRect().top - parseInt(getComputedStyle(document.body).marginTop)) 292 return window.pageYOffset || (document.documentElement || document.body).scrollTop 293 } 294 295 function widgetTopHeight(lineObj) { 296 let height = 0 297 if (lineObj.widgets) for (let i = 0; i < lineObj.widgets.length; ++i) if (lineObj.widgets[i].above) 298 height += widgetHeight(lineObj.widgets[i]) 299 return height 300 } 301 302 // Converts a {top, bottom, left, right} box from line-local 303 // coordinates into another coordinate system. Context may be one of 304 // "line", "div" (display.lineDiv), "local"./null (editor), "window", 305 // or "page". 306 export function intoCoordSystem(cm, lineObj, rect, context, includeWidgets) { 307 if (!includeWidgets) { 308 let height = widgetTopHeight(lineObj) 309 rect.top += height; rect.bottom += height 310 } 311 if (context == "line") return rect 312 if (!context) context = "local" 313 let yOff = heightAtLine(lineObj) 314 if (context == "local") yOff += paddingTop(cm.display) 315 else yOff -= cm.display.viewOffset 316 if (context == "page" || context == "window") { 317 let lOff = cm.display.lineSpace.getBoundingClientRect() 318 yOff += lOff.top + (context == "window" ? 0 : pageScrollY()) 319 let xOff = lOff.left + (context == "window" ? 0 : pageScrollX()) 320 rect.left += xOff; rect.right += xOff 321 } 322 rect.top += yOff; rect.bottom += yOff 323 return rect 324 } 325 326 // Coverts a box from "div" coords to another coordinate system. 327 // Context may be "window", "page", "div", or "local"./null. 328 export function fromCoordSystem(cm, coords, context) { 329 if (context == "div") return coords 330 let left = coords.left, top = coords.top 331 // First move into "page" coordinate system 332 if (context == "page") { 333 left -= pageScrollX() 334 top -= pageScrollY() 335 } else if (context == "local" || !context) { 336 let localBox = cm.display.sizer.getBoundingClientRect() 337 left += localBox.left 338 top += localBox.top 339 } 340 341 let lineSpaceBox = cm.display.lineSpace.getBoundingClientRect() 342 return {left: left - lineSpaceBox.left, top: top - lineSpaceBox.top} 343 } 344 345 export function charCoords(cm, pos, context, lineObj, bias) { 346 if (!lineObj) lineObj = getLine(cm.doc, pos.line) 347 return intoCoordSystem(cm, lineObj, measureChar(cm, lineObj, pos.ch, bias), context) 348 } 349 350 // Returns a box for a given cursor position, which may have an 351 // 'other' property containing the position of the secondary cursor 352 // on a bidi boundary. 353 // A cursor Pos(line, char, "before") is on the same visual line as `char - 1` 354 // and after `char - 1` in writing order of `char - 1` 355 // A cursor Pos(line, char, "after") is on the same visual line as `char` 356 // and before `char` in writing order of `char` 357 // Examples (upper-case letters are RTL, lower-case are LTR): 358 // Pos(0, 1, ...) 359 // before after 360 // ab a|b a|b 361 // aB a|B aB| 362 // Ab |Ab A|b 363 // AB B|A B|A 364 // Every position after the last character on a line is considered to stick 365 // to the last character on the line. 366 export function cursorCoords(cm, pos, context, lineObj, preparedMeasure, varHeight) { 367 lineObj = lineObj || getLine(cm.doc, pos.line) 368 if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj) 369 function get(ch, right) { 370 let m = measureCharPrepared(cm, preparedMeasure, ch, right ? "right" : "left", varHeight) 371 if (right) m.left = m.right; else m.right = m.left 372 return intoCoordSystem(cm, lineObj, m, context) 373 } 374 let order = getOrder(lineObj, cm.doc.direction), ch = pos.ch, sticky = pos.sticky 375 if (ch >= lineObj.text.length) { 376 ch = lineObj.text.length 377 sticky = "before" 378 } else if (ch <= 0) { 379 ch = 0 380 sticky = "after" 381 } 382 if (!order) return get(sticky == "before" ? ch - 1 : ch, sticky == "before") 383 384 function getBidi(ch, partPos, invert) { 385 let part = order[partPos], right = part.level == 1 386 return get(invert ? ch - 1 : ch, right != invert) 387 } 388 let partPos = getBidiPartAt(order, ch, sticky) 389 let other = bidiOther 390 let val = getBidi(ch, partPos, sticky == "before") 391 if (other != null) val.other = getBidi(ch, other, sticky != "before") 392 return val 393 } 394 395 // Used to cheaply estimate the coordinates for a position. Used for 396 // intermediate scroll updates. 397 export function estimateCoords(cm, pos) { 398 let left = 0 399 pos = clipPos(cm.doc, pos) 400 if (!cm.options.lineWrapping) left = charWidth(cm.display) * pos.ch 401 let lineObj = getLine(cm.doc, pos.line) 402 let top = heightAtLine(lineObj) + paddingTop(cm.display) 403 return {left: left, right: left, top: top, bottom: top + lineObj.height} 404 } 405 406 // Positions returned by coordsChar contain some extra information. 407 // xRel is the relative x position of the input coordinates compared 408 // to the found position (so xRel > 0 means the coordinates are to 409 // the right of the character position, for example). When outside 410 // is true, that means the coordinates lie outside the line's 411 // vertical range. 412 function PosWithInfo(line, ch, sticky, outside, xRel) { 413 let pos = Pos(line, ch, sticky) 414 pos.xRel = xRel 415 if (outside) pos.outside = true 416 return pos 417 } 418 419 // Compute the character position closest to the given coordinates. 420 // Input must be lineSpace-local ("div" coordinate system). 421 export function coordsChar(cm, x, y) { 422 let doc = cm.doc 423 y += cm.display.viewOffset 424 if (y < 0) return PosWithInfo(doc.first, 0, null, true, -1) 425 let lineN = lineAtHeight(doc, y), last = doc.first + doc.size - 1 426 if (lineN > last) 427 return PosWithInfo(doc.first + doc.size - 1, getLine(doc, last).text.length, null, true, 1) 428 if (x < 0) x = 0 429 430 let lineObj = getLine(doc, lineN) 431 for (;;) { 432 let found = coordsCharInner(cm, lineObj, lineN, x, y) 433 let merged = collapsedSpanAtEnd(lineObj) 434 let mergedPos = merged && merged.find(0, true) 435 if (merged && (found.ch > mergedPos.from.ch || found.ch == mergedPos.from.ch && found.xRel > 0)) 436 lineN = lineNo(lineObj = mergedPos.to.line) 437 else 438 return found 439 } 440 } 441 442 function wrappedLineExtent(cm, lineObj, preparedMeasure, y) { 443 y -= widgetTopHeight(lineObj) 444 let end = lineObj.text.length 445 let begin = findFirst(ch => measureCharPrepared(cm, preparedMeasure, ch - 1).bottom <= y, end, 0) 446 end = findFirst(ch => measureCharPrepared(cm, preparedMeasure, ch).top > y, begin, end) 447 return {begin, end} 448 } 449 450 export function wrappedLineExtentChar(cm, lineObj, preparedMeasure, target) { 451 if (!preparedMeasure) preparedMeasure = prepareMeasureForLine(cm, lineObj) 452 let targetTop = intoCoordSystem(cm, lineObj, measureCharPrepared(cm, preparedMeasure, target), "line").top 453 return wrappedLineExtent(cm, lineObj, preparedMeasure, targetTop) 454 } 455 456 // Returns true if the given side of a box is after the given 457 // coordinates, in top-to-bottom, left-to-right order. 458 function boxIsAfter(box, x, y, left) { 459 return box.bottom <= y ? false : box.top > y ? true : (left ? box.left : box.right) > x 460 } 461 462 function coordsCharInner(cm, lineObj, lineNo, x, y) { 463 // Move y into line-local coordinate space 464 y -= heightAtLine(lineObj) 465 let preparedMeasure = prepareMeasureForLine(cm, lineObj) 466 // When directly calling `measureCharPrepared`, we have to adjust 467 // for the widgets at this line. 468 let widgetHeight = widgetTopHeight(lineObj) 469 let begin = 0, end = lineObj.text.length, ltr = true 470 471 let order = getOrder(lineObj, cm.doc.direction) 472 // If the line isn't plain left-to-right text, first figure out 473 // which bidi section the coordinates fall into. 474 if (order) { 475 let part = (cm.options.lineWrapping ? coordsBidiPartWrapped : coordsBidiPart) 476 (cm, lineObj, lineNo, preparedMeasure, order, x, y) 477 ltr = part.level != 1 478 // The awkward -1 offsets are needed because findFirst (called 479 // on these below) will treat its first bound as inclusive, 480 // second as exclusive, but we want to actually address the 481 // characters in the part's range 482 begin = ltr ? part.from : part.to - 1 483 end = ltr ? part.to : part.from - 1 484 } 485 486 // A binary search to find the first character whose bounding box 487 // starts after the coordinates. If we run across any whose box wrap 488 // the coordinates, store that. 489 let chAround = null, boxAround = null 490 let ch = findFirst(ch => { 491 let box = measureCharPrepared(cm, preparedMeasure, ch) 492 box.top += widgetHeight; box.bottom += widgetHeight 493 if (!boxIsAfter(box, x, y, false)) return false 494 if (box.top <= y && box.left <= x) { 495 chAround = ch 496 boxAround = box 497 } 498 return true 499 }, begin, end) 500 501 let baseX, sticky, outside = false 502 // If a box around the coordinates was found, use that 503 if (boxAround) { 504 // Distinguish coordinates nearer to the left or right side of the box 505 let atLeft = x - boxAround.left < boxAround.right - x, atStart = atLeft == ltr 506 ch = chAround + (atStart ? 0 : 1) 507 sticky = atStart ? "after" : "before" 508 baseX = atLeft ? boxAround.left : boxAround.right 509 } else { 510 // (Adjust for extended bound, if necessary.) 511 if (!ltr && (ch == end || ch == begin)) ch++ 512 // To determine which side to associate with, get the box to the 513 // left of the character and compare it's vertical position to the 514 // coordinates 515 sticky = ch == 0 ? "after" : ch == lineObj.text.length ? "before" : 516 (measureCharPrepared(cm, preparedMeasure, ch - (ltr ? 1 : 0)).bottom + widgetHeight <= y) == ltr ? 517 "after" : "before" 518 // Now get accurate coordinates for this place, in order to get a 519 // base X position 520 let coords = cursorCoords(cm, Pos(lineNo, ch, sticky), "line", lineObj, preparedMeasure) 521 baseX = coords.left 522 outside = y < coords.top || y >= coords.bottom 523 } 524 525 ch = skipExtendingChars(lineObj.text, ch, 1) 526 return PosWithInfo(lineNo, ch, sticky, outside, x - baseX) 527 } 528 529 function coordsBidiPart(cm, lineObj, lineNo, preparedMeasure, order, x, y) { 530 // Bidi parts are sorted left-to-right, and in a non-line-wrapping 531 // situation, we can take this ordering to correspond to the visual 532 // ordering. This finds the first part whose end is after the given 533 // coordinates. 534 let index = findFirst(i => { 535 let part = order[i], ltr = part.level != 1 536 return boxIsAfter(cursorCoords(cm, Pos(lineNo, ltr ? part.to : part.from, ltr ? "before" : "after"), 537 "line", lineObj, preparedMeasure), x, y, true) 538 }, 0, order.length - 1) 539 let part = order[index] 540 // If this isn't the first part, the part's start is also after 541 // the coordinates, and the coordinates aren't on the same line as 542 // that start, move one part back. 543 if (index > 0) { 544 let ltr = part.level != 1 545 let start = cursorCoords(cm, Pos(lineNo, ltr ? part.from : part.to, ltr ? "after" : "before"), 546 "line", lineObj, preparedMeasure) 547 if (boxIsAfter(start, x, y, true) && start.top > y) 548 part = order[index - 1] 549 } 550 return part 551 } 552 553 function coordsBidiPartWrapped(cm, lineObj, _lineNo, preparedMeasure, order, x, y) { 554 // In a wrapped line, rtl text on wrapping boundaries can do things 555 // that don't correspond to the ordering in our `order` array at 556 // all, so a binary search doesn't work, and we want to return a 557 // part that only spans one line so that the binary search in 558 // coordsCharInner is safe. As such, we first find the extent of the 559 // wrapped line, and then do a flat search in which we discard any 560 // spans that aren't on the line. 561 let {begin, end} = wrappedLineExtent(cm, lineObj, preparedMeasure, y) 562 if (/\s/.test(lineObj.text.charAt(end - 1))) end-- 563 let part = null, closestDist = null 564 for (let i = 0; i < order.length; i++) { 565 let p = order[i] 566 if (p.from >= end || p.to <= begin) continue 567 let ltr = p.level != 1 568 let endX = measureCharPrepared(cm, preparedMeasure, ltr ? Math.min(end, p.to) - 1 : Math.max(begin, p.from)).right 569 // Weigh against spans ending before this, so that they are only 570 // picked if nothing ends after 571 let dist = endX < x ? x - endX + 1e9 : endX - x 572 if (!part || closestDist > dist) { 573 part = p 574 closestDist = dist 575 } 576 } 577 if (!part) part = order[order.length - 1] 578 // Clip the part to the wrapped line. 579 if (part.from < begin) part = {from: begin, to: part.to, level: part.level} 580 if (part.to > end) part = {from: part.from, to: end, level: part.level} 581 return part 582 } 583 584 let measureText 585 // Compute the default text height. 586 export function textHeight(display) { 587 if (display.cachedTextHeight != null) return display.cachedTextHeight 588 if (measureText == null) { 589 measureText = elt("pre") 590 // Measure a bunch of lines, for browsers that compute 591 // fractional heights. 592 for (let i = 0; i < 49; ++i) { 593 measureText.appendChild(document.createTextNode("x")) 594 measureText.appendChild(elt("br")) 595 } 596 measureText.appendChild(document.createTextNode("x")) 597 } 598 removeChildrenAndAdd(display.measure, measureText) 599 let height = measureText.offsetHeight / 50 600 if (height > 3) display.cachedTextHeight = height 601 removeChildren(display.measure) 602 return height || 1 603 } 604 605 // Compute the default character width. 606 export function charWidth(display) { 607 if (display.cachedCharWidth != null) return display.cachedCharWidth 608 let anchor = elt("span", "xxxxxxxxxx") 609 let pre = elt("pre", [anchor]) 610 removeChildrenAndAdd(display.measure, pre) 611 let rect = anchor.getBoundingClientRect(), width = (rect.right - rect.left) / 10 612 if (width > 2) display.cachedCharWidth = width 613 return width || 10 614 } 615 616 // Do a bulk-read of the DOM positions and sizes needed to draw the 617 // view, so that we don't interleave reading and writing to the DOM. 618 export function getDimensions(cm) { 619 let d = cm.display, left = {}, width = {} 620 let gutterLeft = d.gutters.clientLeft 621 for (let n = d.gutters.firstChild, i = 0; n; n = n.nextSibling, ++i) { 622 left[cm.options.gutters[i]] = n.offsetLeft + n.clientLeft + gutterLeft 623 width[cm.options.gutters[i]] = n.clientWidth 624 } 625 return {fixedPos: compensateForHScroll(d), 626 gutterTotalWidth: d.gutters.offsetWidth, 627 gutterLeft: left, 628 gutterWidth: width, 629 wrapperWidth: d.wrapper.clientWidth} 630 } 631 632 // Computes display.scroller.scrollLeft + display.gutters.offsetWidth, 633 // but using getBoundingClientRect to get a sub-pixel-accurate 634 // result. 635 export function compensateForHScroll(display) { 636 return display.scroller.getBoundingClientRect().left - display.sizer.getBoundingClientRect().left 637 } 638 639 // Returns a function that estimates the height of a line, to use as 640 // first approximation until the line becomes visible (and is thus 641 // properly measurable). 642 export function estimateHeight(cm) { 643 let th = textHeight(cm.display), wrapping = cm.options.lineWrapping 644 let perLine = wrapping && Math.max(5, cm.display.scroller.clientWidth / charWidth(cm.display) - 3) 645 return line => { 646 if (lineIsHidden(cm.doc, line)) return 0 647 648 let widgetsHeight = 0 649 if (line.widgets) for (let i = 0; i < line.widgets.length; i++) { 650 if (line.widgets[i].height) widgetsHeight += line.widgets[i].height 651 } 652 653 if (wrapping) 654 return widgetsHeight + (Math.ceil(line.text.length / perLine) || 1) * th 655 else 656 return widgetsHeight + th 657 } 658 } 659 660 export function estimateLineHeights(cm) { 661 let doc = cm.doc, est = estimateHeight(cm) 662 doc.iter(line => { 663 let estHeight = est(line) 664 if (estHeight != line.height) updateLineHeight(line, estHeight) 665 }) 666 } 667 668 // Given a mouse event, find the corresponding position. If liberal 669 // is false, it checks whether a gutter or scrollbar was clicked, 670 // and returns null if it was. forRect is used by rectangular 671 // selections, and tries to estimate a character position even for 672 // coordinates beyond the right of the text. 673 export function posFromMouse(cm, e, liberal, forRect) { 674 let display = cm.display 675 if (!liberal && e_target(e).getAttribute("cm-not-content") == "true") return null 676 677 let x, y, space = display.lineSpace.getBoundingClientRect() 678 // Fails unpredictably on IE[67] when mouse is dragged around quickly. 679 try { x = e.clientX - space.left; y = e.clientY - space.top } 680 catch (e) { return null } 681 let coords = coordsChar(cm, x, y), line 682 if (forRect && coords.xRel == 1 && (line = getLine(cm.doc, coords.line).text).length == coords.ch) { 683 let colDiff = countColumn(line, line.length, cm.options.tabSize) - line.length 684 coords = Pos(coords.line, Math.max(0, Math.round((x - paddingH(cm.display).left) / charWidth(cm.display)) - colDiff)) 685 } 686 return coords 687 } 688 689 // Find the view element corresponding to a given line. Return null 690 // when the line isn't visible. 691 export function findViewIndex(cm, n) { 692 if (n >= cm.display.viewTo) return null 693 n -= cm.display.viewFrom 694 if (n < 0) return null 695 let view = cm.display.view 696 for (let i = 0; i < view.length; i++) { 697 n -= view[i].size 698 if (n < 0) return i 699 } 700 }