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