1package tview
2
3import (
4	"math"
5	"regexp"
6	"sort"
7	"strconv"
8
9	"github.com/gdamore/tcell"
10	runewidth "github.com/mattn/go-runewidth"
11	"github.com/rivo/uniseg"
12)
13
14// Text alignment within a box.
15const (
16	AlignLeft = iota
17	AlignCenter
18	AlignRight
19)
20
21// Common regular expressions.
22var (
23	colorPattern     = regexp.MustCompile(`\[([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([a-zA-Z]+|#[0-9a-zA-Z]{6}|\-)?(:([lbdru]+|\-)?)?)?\]`)
24	regionPattern    = regexp.MustCompile(`\["([a-zA-Z0-9_,;: \-\.]*)"\]`)
25	escapePattern    = regexp.MustCompile(`\[([a-zA-Z0-9_,;: \-\."#]+)\[(\[*)\]`)
26	nonEscapePattern = regexp.MustCompile(`(\[[a-zA-Z0-9_,;: \-\."#]+\[*)\]`)
27	boundaryPattern  = regexp.MustCompile(`(([,\.\-:;!\?&#+]|\n)[ \t\f\r]*|([ \t\f\r]+))`)
28	spacePattern     = regexp.MustCompile(`\s+`)
29)
30
31// Positions of substrings in regular expressions.
32const (
33	colorForegroundPos = 1
34	colorBackgroundPos = 3
35	colorFlagPos       = 5
36)
37
38// Predefined InputField acceptance functions.
39var (
40	// InputFieldInteger accepts integers.
41	InputFieldInteger func(text string, ch rune) bool
42
43	// InputFieldFloat accepts floating-point numbers.
44	InputFieldFloat func(text string, ch rune) bool
45
46	// InputFieldMaxLength returns an input field accept handler which accepts
47	// input strings up to a given length. Use it like this:
48	//
49	//   inputField.SetAcceptanceFunc(InputFieldMaxLength(10)) // Accept up to 10 characters.
50	InputFieldMaxLength func(maxLength int) func(text string, ch rune) bool
51)
52
53// Package initialization.
54func init() {
55	// We'll use zero width joiners.
56	runewidth.ZeroWidthJoiner = true
57
58	// Initialize the predefined input field handlers.
59	InputFieldInteger = func(text string, ch rune) bool {
60		if text == "-" {
61			return true
62		}
63		_, err := strconv.Atoi(text)
64		return err == nil
65	}
66	InputFieldFloat = func(text string, ch rune) bool {
67		if text == "-" || text == "." || text == "-." {
68			return true
69		}
70		_, err := strconv.ParseFloat(text, 64)
71		return err == nil
72	}
73	InputFieldMaxLength = func(maxLength int) func(text string, ch rune) bool {
74		return func(text string, ch rune) bool {
75			return len([]rune(text)) <= maxLength
76		}
77	}
78}
79
80// styleFromTag takes the given style, defined by a foreground color (fgColor),
81// a background color (bgColor), and style attributes, and modifies it based on
82// the substrings (tagSubstrings) extracted by the regular expression for color
83// tags. The new colors and attributes are returned where empty strings mean
84// "don't modify" and a dash ("-") means "reset to default".
85func styleFromTag(fgColor, bgColor, attributes string, tagSubstrings []string) (newFgColor, newBgColor, newAttributes string) {
86	if tagSubstrings[colorForegroundPos] != "" {
87		color := tagSubstrings[colorForegroundPos]
88		if color == "-" {
89			fgColor = "-"
90		} else if color != "" {
91			fgColor = color
92		}
93	}
94
95	if tagSubstrings[colorBackgroundPos-1] != "" {
96		color := tagSubstrings[colorBackgroundPos]
97		if color == "-" {
98			bgColor = "-"
99		} else if color != "" {
100			bgColor = color
101		}
102	}
103
104	if tagSubstrings[colorFlagPos-1] != "" {
105		flags := tagSubstrings[colorFlagPos]
106		if flags == "-" {
107			attributes = "-"
108		} else if flags != "" {
109			attributes = flags
110		}
111	}
112
113	return fgColor, bgColor, attributes
114}
115
116// overlayStyle mixes a background color with a foreground color (fgColor),
117// a (possibly new) background color (bgColor), and style attributes, and
118// returns the resulting style. For a definition of the colors and attributes,
119// see styleFromTag(). Reset instructions cause the corresponding part of the
120// default style to be used.
121func overlayStyle(background tcell.Color, defaultStyle tcell.Style, fgColor, bgColor, attributes string) tcell.Style {
122	defFg, defBg, defAttr := defaultStyle.Decompose()
123	style := defaultStyle.Background(background)
124
125	style = style.Foreground(defFg)
126	if fgColor != "" {
127		if fgColor == "-" {
128			style = style.Foreground(defFg)
129		} else {
130			style = style.Foreground(tcell.GetColor(fgColor))
131		}
132	}
133
134	if bgColor == "-" || bgColor == "" && defBg != tcell.ColorDefault {
135		style = style.Background(defBg)
136	} else if bgColor != "" {
137		style = style.Background(tcell.GetColor(bgColor))
138	}
139
140	if attributes == "-" {
141		style = style.Bold(defAttr&tcell.AttrBold > 0)
142		style = style.Blink(defAttr&tcell.AttrBlink > 0)
143		style = style.Reverse(defAttr&tcell.AttrReverse > 0)
144		style = style.Underline(defAttr&tcell.AttrUnderline > 0)
145		style = style.Dim(defAttr&tcell.AttrDim > 0)
146	} else if attributes != "" {
147		style = style.Normal()
148		for _, flag := range attributes {
149			switch flag {
150			case 'l':
151				style = style.Blink(true)
152			case 'b':
153				style = style.Bold(true)
154			case 'd':
155				style = style.Dim(true)
156			case 'r':
157				style = style.Reverse(true)
158			case 'u':
159				style = style.Underline(true)
160			}
161		}
162	}
163
164	return style
165}
166
167// decomposeString returns information about a string which may contain color
168// tags or region tags, depending on which ones are requested to be found. It
169// returns the indices of the color tags (as returned by
170// re.FindAllStringIndex()), the color tags themselves (as returned by
171// re.FindAllStringSubmatch()), the indices of region tags and the region tags
172// themselves, the indices of an escaped tags (only if at least color tags or
173// region tags are requested), the string stripped by any tags and escaped, and
174// the screen width of the stripped string.
175func decomposeString(text string, findColors, findRegions bool) (colorIndices [][]int, colors [][]string, regionIndices [][]int, regions [][]string, escapeIndices [][]int, stripped string, width int) {
176	// Shortcut for the trivial case.
177	if !findColors && !findRegions {
178		return nil, nil, nil, nil, nil, text, stringWidth(text)
179	}
180
181	// Get positions of any tags.
182	if findColors {
183		colorIndices = colorPattern.FindAllStringIndex(text, -1)
184		colors = colorPattern.FindAllStringSubmatch(text, -1)
185	}
186	if findRegions {
187		regionIndices = regionPattern.FindAllStringIndex(text, -1)
188		regions = regionPattern.FindAllStringSubmatch(text, -1)
189	}
190	escapeIndices = escapePattern.FindAllStringIndex(text, -1)
191
192	// Because the color pattern detects empty tags, we need to filter them out.
193	for i := len(colorIndices) - 1; i >= 0; i-- {
194		if colorIndices[i][1]-colorIndices[i][0] == 2 {
195			colorIndices = append(colorIndices[:i], colorIndices[i+1:]...)
196			colors = append(colors[:i], colors[i+1:]...)
197		}
198	}
199
200	// Make a (sorted) list of all tags.
201	allIndices := make([][3]int, 0, len(colorIndices)+len(regionIndices)+len(escapeIndices))
202	for indexType, index := range [][][]int{colorIndices, regionIndices, escapeIndices} {
203		for _, tag := range index {
204			allIndices = append(allIndices, [3]int{tag[0], tag[1], indexType})
205		}
206	}
207	sort.Slice(allIndices, func(i int, j int) bool {
208		return allIndices[i][0] < allIndices[j][0]
209	})
210
211	// Remove the tags from the original string.
212	var from int
213	buf := make([]byte, 0, len(text))
214	for _, indices := range allIndices {
215		if indices[2] == 2 { // Escape sequences are not simply removed.
216			buf = append(buf, []byte(text[from:indices[1]-2])...)
217			buf = append(buf, ']')
218			from = indices[1]
219		} else {
220			buf = append(buf, []byte(text[from:indices[0]])...)
221			from = indices[1]
222		}
223	}
224	buf = append(buf, text[from:]...)
225	stripped = string(buf)
226
227	// Get the width of the stripped string.
228	width = stringWidth(stripped)
229
230	return
231}
232
233// Print prints text onto the screen into the given box at (x,y,maxWidth,1),
234// not exceeding that box. "align" is one of AlignLeft, AlignCenter, or
235// AlignRight. The screen's background color will not be changed.
236//
237// You can change the colors and text styles mid-text by inserting a color tag.
238// See the package description for details.
239//
240// Returns the number of actual bytes of the text printed (including color tags)
241// and the actual width used for the printed runes.
242func Print(screen tcell.Screen, text string, x, y, maxWidth, align int, color tcell.Color) (int, int) {
243	return printWithStyle(screen, text, x, y, maxWidth, align, tcell.StyleDefault.Foreground(color))
244}
245
246// printWithStyle works like Print() but it takes a style instead of just a
247// foreground color.
248func printWithStyle(screen tcell.Screen, text string, x, y, maxWidth, align int, style tcell.Style) (int, int) {
249	totalWidth, totalHeight := screen.Size()
250	if maxWidth <= 0 || len(text) == 0 || y < 0 || y >= totalHeight {
251		return 0, 0
252	}
253
254	// Decompose the text.
255	colorIndices, colors, _, _, escapeIndices, strippedText, strippedWidth := decomposeString(text, true, false)
256
257	// We want to reduce all alignments to AlignLeft.
258	if align == AlignRight {
259		if strippedWidth <= maxWidth {
260			// There's enough space for the entire text.
261			return printWithStyle(screen, text, x+maxWidth-strippedWidth, y, maxWidth, AlignLeft, style)
262		}
263		// Trim characters off the beginning.
264		var (
265			bytes, width, colorPos, escapePos, tagOffset int
266			foregroundColor, backgroundColor, attributes string
267		)
268		_, originalBackground, _ := style.Decompose()
269		iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
270			// Update color/escape tag offset and style.
271			if colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
272				foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
273				style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
274				tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
275				colorPos++
276			}
277			if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
278				tagOffset++
279				escapePos++
280			}
281			if strippedWidth-screenPos < maxWidth {
282				// We chopped off enough.
283				if escapePos > 0 && textPos+tagOffset-1 >= escapeIndices[escapePos-1][0] && textPos+tagOffset-1 < escapeIndices[escapePos-1][1] {
284					// Unescape open escape sequences.
285					escapeCharPos := escapeIndices[escapePos-1][1] - 2
286					text = text[:escapeCharPos] + text[escapeCharPos+1:]
287				}
288				// Print and return.
289				bytes, width = printWithStyle(screen, text[textPos+tagOffset:], x, y, maxWidth, AlignLeft, style)
290				return true
291			}
292			return false
293		})
294		return bytes, width
295	} else if align == AlignCenter {
296		if strippedWidth == maxWidth {
297			// Use the exact space.
298			return printWithStyle(screen, text, x, y, maxWidth, AlignLeft, style)
299		} else if strippedWidth < maxWidth {
300			// We have more space than we need.
301			half := (maxWidth - strippedWidth) / 2
302			return printWithStyle(screen, text, x+half, y, maxWidth-half, AlignLeft, style)
303		} else {
304			// Chop off runes until we have a perfect fit.
305			var choppedLeft, choppedRight, leftIndex, rightIndex int
306			rightIndex = len(strippedText)
307			for rightIndex-1 > leftIndex && strippedWidth-choppedLeft-choppedRight > maxWidth {
308				if choppedLeft < choppedRight {
309					// Iterate on the left by one character.
310					iterateString(strippedText[leftIndex:], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
311						choppedLeft += screenWidth
312						leftIndex += textWidth
313						return true
314					})
315				} else {
316					// Iterate on the right by one character.
317					iterateStringReverse(strippedText[leftIndex:rightIndex], func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
318						choppedRight += screenWidth
319						rightIndex -= textWidth
320						return true
321					})
322				}
323			}
324
325			// Add tag offsets and determine start style.
326			var (
327				colorPos, escapePos, tagOffset               int
328				foregroundColor, backgroundColor, attributes string
329			)
330			_, originalBackground, _ := style.Decompose()
331			for index := range strippedText {
332				// We only need the offset of the left index.
333				if index > leftIndex {
334					// We're done.
335					if escapePos > 0 && leftIndex+tagOffset-1 >= escapeIndices[escapePos-1][0] && leftIndex+tagOffset-1 < escapeIndices[escapePos-1][1] {
336						// Unescape open escape sequences.
337						escapeCharPos := escapeIndices[escapePos-1][1] - 2
338						text = text[:escapeCharPos] + text[escapeCharPos+1:]
339					}
340					break
341				}
342
343				// Update color/escape tag offset.
344				if colorPos < len(colorIndices) && index+tagOffset >= colorIndices[colorPos][0] && index+tagOffset < colorIndices[colorPos][1] {
345					if index <= leftIndex {
346						foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
347						style = overlayStyle(originalBackground, style, foregroundColor, backgroundColor, attributes)
348					}
349					tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
350					colorPos++
351				}
352				if escapePos < len(escapeIndices) && index+tagOffset >= escapeIndices[escapePos][0] && index+tagOffset < escapeIndices[escapePos][1] {
353					tagOffset++
354					escapePos++
355				}
356			}
357			return printWithStyle(screen, text[leftIndex+tagOffset:], x, y, maxWidth, AlignLeft, style)
358		}
359	}
360
361	// Draw text.
362	var (
363		drawn, drawnWidth, colorPos, escapePos, tagOffset int
364		foregroundColor, backgroundColor, attributes      string
365	)
366	iterateString(strippedText, func(main rune, comb []rune, textPos, length, screenPos, screenWidth int) bool {
367		// Only continue if there is still space.
368		if drawnWidth+screenWidth > maxWidth || x+drawnWidth >= totalWidth {
369			return true
370		}
371
372		// Handle color tags.
373		for colorPos < len(colorIndices) && textPos+tagOffset >= colorIndices[colorPos][0] && textPos+tagOffset < colorIndices[colorPos][1] {
374			foregroundColor, backgroundColor, attributes = styleFromTag(foregroundColor, backgroundColor, attributes, colors[colorPos])
375			tagOffset += colorIndices[colorPos][1] - colorIndices[colorPos][0]
376			colorPos++
377		}
378
379		// Handle scape tags.
380		if escapePos < len(escapeIndices) && textPos+tagOffset >= escapeIndices[escapePos][0] && textPos+tagOffset < escapeIndices[escapePos][1] {
381			if textPos+tagOffset == escapeIndices[escapePos][1]-2 {
382				tagOffset++
383				escapePos++
384			}
385		}
386
387		// Print the rune sequence.
388		finalX := x + drawnWidth
389		_, _, finalStyle, _ := screen.GetContent(finalX, y)
390		_, background, _ := finalStyle.Decompose()
391		finalStyle = overlayStyle(background, style, foregroundColor, backgroundColor, attributes)
392		for offset := screenWidth - 1; offset >= 0; offset-- {
393			// To avoid undesired effects, we populate all cells.
394			if offset == 0 {
395				screen.SetContent(finalX+offset, y, main, comb, finalStyle)
396			} else {
397				screen.SetContent(finalX+offset, y, ' ', nil, finalStyle)
398			}
399		}
400
401		// Advance.
402		drawn += length
403		drawnWidth += screenWidth
404
405		return false
406	})
407
408	return drawn + tagOffset + len(escapeIndices), drawnWidth
409}
410
411// PrintSimple prints white text to the screen at the given position.
412func PrintSimple(screen tcell.Screen, text string, x, y int) {
413	Print(screen, text, x, y, math.MaxInt32, AlignLeft, Styles.PrimaryTextColor)
414}
415
416// TaggedStringWidth returns the width of the given string needed to print it on
417// screen. The text may contain color tags which are not counted.
418func TaggedStringWidth(text string) int {
419	_, _, _, _, _, _, width := decomposeString(text, true, false)
420	return width
421}
422
423// stringWidth returns the number of horizontal cells needed to print the given
424// text. It splits the text into its grapheme clusters, calculates each
425// cluster's width, and adds them up to a total.
426func stringWidth(text string) (width int) {
427	g := uniseg.NewGraphemes(text)
428	for g.Next() {
429		var chWidth int
430		for _, r := range g.Runes() {
431			chWidth = runewidth.RuneWidth(r)
432			if chWidth > 0 {
433				break // Our best guess at this point is to use the width of the first non-zero-width rune.
434			}
435		}
436		width += chWidth
437	}
438	return
439}
440
441// WordWrap splits a text such that each resulting line does not exceed the
442// given screen width. Possible split points are after any punctuation or
443// whitespace. Whitespace after split points will be dropped.
444//
445// This function considers color tags to have no width.
446//
447// Text is always split at newline characters ('\n').
448func WordWrap(text string, width int) (lines []string) {
449	colorTagIndices, _, _, _, escapeIndices, strippedText, _ := decomposeString(text, true, false)
450
451	// Find candidate breakpoints.
452	breakpoints := boundaryPattern.FindAllStringSubmatchIndex(strippedText, -1)
453	// Results in one entry for each candidate. Each entry is an array a of
454	// indices into strippedText where a[6] < 0 for newline/punctuation matches
455	// and a[4] < 0 for whitespace matches.
456
457	// Process stripped text one character at a time.
458	var (
459		colorPos, escapePos, breakpointPos, tagOffset      int
460		lastBreakpoint, lastContinuation, currentLineStart int
461		lineWidth, overflow                                int
462		forceBreak                                         bool
463	)
464	unescape := func(substr string, startIndex int) string {
465		// A helper function to unescape escaped tags.
466		for index := escapePos; index >= 0; index-- {
467			if index < len(escapeIndices) && startIndex > escapeIndices[index][0] && startIndex < escapeIndices[index][1]-1 {
468				pos := escapeIndices[index][1] - 2 - startIndex
469				return substr[:pos] + substr[pos+1:]
470			}
471		}
472		return substr
473	}
474	iterateString(strippedText, func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool {
475		// Handle tags.
476		for {
477			if colorPos < len(colorTagIndices) && textPos+tagOffset >= colorTagIndices[colorPos][0] && textPos+tagOffset < colorTagIndices[colorPos][1] {
478				// Colour tags.
479				tagOffset += colorTagIndices[colorPos][1] - colorTagIndices[colorPos][0]
480				colorPos++
481			} else if escapePos < len(escapeIndices) && textPos+tagOffset == escapeIndices[escapePos][1]-2 {
482				// Escape tags.
483				tagOffset++
484				escapePos++
485			} else {
486				break
487			}
488		}
489
490		// Is this a breakpoint?
491		if breakpointPos < len(breakpoints) && textPos+tagOffset == breakpoints[breakpointPos][0] {
492			// Yes, it is. Set up breakpoint infos depending on its type.
493			lastBreakpoint = breakpoints[breakpointPos][0] + tagOffset
494			lastContinuation = breakpoints[breakpointPos][1] + tagOffset
495			overflow = 0
496			forceBreak = main == '\n'
497			if breakpoints[breakpointPos][6] < 0 && !forceBreak {
498				lastBreakpoint++ // Don't skip punctuation.
499			}
500			breakpointPos++
501		}
502
503		// Check if a break is warranted.
504		if forceBreak || lineWidth > 0 && lineWidth+screenWidth > width {
505			breakpoint := lastBreakpoint
506			continuation := lastContinuation
507			if forceBreak {
508				breakpoint = textPos + tagOffset
509				continuation = textPos + tagOffset + 1
510				lastBreakpoint = 0
511				overflow = 0
512			} else if lastBreakpoint <= currentLineStart {
513				breakpoint = textPos + tagOffset
514				continuation = textPos + tagOffset
515				overflow = 0
516			}
517			lines = append(lines, unescape(text[currentLineStart:breakpoint], currentLineStart))
518			currentLineStart, lineWidth, forceBreak = continuation, overflow, false
519		}
520
521		// Remember the characters since the last breakpoint.
522		if lastBreakpoint > 0 && lastContinuation <= textPos+tagOffset {
523			overflow += screenWidth
524		}
525
526		// Advance.
527		lineWidth += screenWidth
528
529		// But if we're still inside a breakpoint, skip next character (whitespace).
530		if textPos+tagOffset < currentLineStart {
531			lineWidth -= screenWidth
532		}
533
534		return false
535	})
536
537	// Flush the rest.
538	if currentLineStart < len(text) {
539		lines = append(lines, unescape(text[currentLineStart:], currentLineStart))
540	}
541
542	return
543}
544
545// Escape escapes the given text such that color and/or region tags are not
546// recognized and substituted by the print functions of this package. For
547// example, to include a tag-like string in a box title or in a TextView:
548//
549//   box.SetTitle(tview.Escape("[squarebrackets]"))
550//   fmt.Fprint(textView, tview.Escape(`["quoted"]`))
551func Escape(text string) string {
552	return nonEscapePattern.ReplaceAllString(text, "$1[]")
553}
554
555// iterateString iterates through the given string one printed character at a
556// time. For each such character, the callback function is called with the
557// Unicode code points of the character (the first rune and any combining runes
558// which may be nil if there aren't any), the starting position (in bytes)
559// within the original string, its length in bytes, the screen position of the
560// character, and the screen width of it. The iteration stops if the callback
561// returns true. This function returns true if the iteration was stopped before
562// the last character.
563func iterateString(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
564	var screenPos int
565
566	gr := uniseg.NewGraphemes(text)
567	for gr.Next() {
568		r := gr.Runes()
569		from, to := gr.Positions()
570		width := stringWidth(gr.Str())
571		var comb []rune
572		if len(r) > 1 {
573			comb = r[1:]
574		}
575
576		if callback(r[0], comb, from, to-from, screenPos, width) {
577			return true
578		}
579
580		screenPos += width
581	}
582
583	return false
584}
585
586// iterateStringReverse iterates through the given string in reverse, starting
587// from the end of the string, one printed character at a time. For each such
588// character, the callback function is called with the Unicode code points of
589// the character (the first rune and any combining runes which may be nil if
590// there aren't any), the starting position (in bytes) within the original
591// string, its length in bytes, the screen position of the character, and the
592// screen width of it. The iteration stops if the callback returns true. This
593// function returns true if the iteration was stopped before the last character.
594func iterateStringReverse(text string, callback func(main rune, comb []rune, textPos, textWidth, screenPos, screenWidth int) bool) bool {
595	type cluster struct {
596		main                                       rune
597		comb                                       []rune
598		textPos, textWidth, screenPos, screenWidth int
599	}
600
601	// Create the grapheme clusters.
602	var clusters []cluster
603	iterateString(text, func(main rune, comb []rune, textPos int, textWidth int, screenPos int, screenWidth int) bool {
604		clusters = append(clusters, cluster{
605			main:        main,
606			comb:        comb,
607			textPos:     textPos,
608			textWidth:   textWidth,
609			screenPos:   screenPos,
610			screenWidth: screenWidth,
611		})
612		return false
613	})
614
615	// Iterate in reverse.
616	for index := len(clusters) - 1; index >= 0; index-- {
617		if callback(
618			clusters[index].main,
619			clusters[index].comb,
620			clusters[index].textPos,
621			clusters[index].textWidth,
622			clusters[index].screenPos,
623			clusters[index].screenWidth,
624		) {
625			return true
626		}
627	}
628
629	return false
630}
631