1 /++
2 	A homemade text layout and editing engine, designed for the needs of minigui's custom widgets to be good enough for me to use. May or may not work for you.
3 
4 
5 	You use it by creating a [TextLayouter] and populating it with some data. Then you connect it to a user interface which calls [TextLayouter.getDrawableText] to know what and where to display the content and manipulates the content through the [Selection] object. Your text has styles applied to it through a [TextStyle] interface, which is deliberately minimal for the layouter - you are expected to cast it back to your implementation as-needed to get your other data out.
6 
7 	See the docs on each of those objects for more details.
8 
9 	Bugs:
10 		BiDi and right-to-left text in general is not yet implemented. I'm pretty sure I can do it, but I need unicode tables that aren't available to arsd yet.
11 
12 		Doesn't do text kerning since the other implementations I've looked at on-screen don't do it either so it seems unnecessary. I might revisit this.
13 
14 		Also doesn't handle shaped text, which breaks click point detection on Windows for certain script families.
15 
16 		The edit implementation is a simple string. It performs surprisingly well, but I'll probably go back to it and change to a gap buffer later.
17 
18 		Relaying out and saving state is only partially incremental at this time.
19 
20 		The main interfaces are written with eventually fixing these in mind, but I might have to extend the [MeasurableFont] and [TextStyle] interfaces, and it might need some helper objects injected too. So possible they will be small breaking changes to support these, but I'm pretty sure it won't require any major rewrites of the code nor of user code when these are added, just adding methods to interfaces.
21 
22 	History:
23 		Written in December 2022. Released in arsd 11.0.
24 +/
25 module arsd.textlayouter;
26 
27 import arsd.simpledisplay;
28 
29 /+
30 	FIXME: caret style might need to be separate from anything drawn.
31 	FIXME: when adding things, inform new sizes for scrollbar updates in real time
32 	FIXME: scroll when selecting and dragging oob. generally capture on mouse down and release on mouse up.
33 	FIXME: page up, page down.
34 
35 	FIXME: there is a spot right between some glyphs when changing fonts where it selected none.
36 
37 
38 	Need to know style at insertion point (which is the one before the caret codepoint unless it is at start of line, in which case it is the one at it)
39 
40 
41 	The style interface might actually want like toHtml and toRtf. at least on the minigui side, not strictly necessary here.
42 +/
43 
44 
45 /+
46 	subclass w/ style
47 	lazy layout queuing
48 
49 	style info could possibly be a linked list but it prolly don't have to be anything too special
50 
51 	track changes
52 +/
53 
54 /+
55 	Word wrap needs to maintain indentation in some cases
56 
57 	The left and right margins of exclusion area
58 
59 	Exclusion are in the center?
60 
61 	line-spacing
62 
63 	if you click on the gap above a bounding box of a segment it doesn't find that segement despite being in the same line. need to check not just by segment bounding box but by line bounding box.
64 
65 	FIXME: in sdpy, font is not reset upon returning from a child painter
66 	FIXME: in minigui the scrollbars can steal focus from the thing the are controlling
67 	FIXME: scw needs a per-button-click scroll amount since 1 may not be sufficient every time (tho 1 should be a possibility somehow)
68 +/
69 
70 /+
71 	REPLACED CONTENT
72 
73 		magic char followed by a dchar
74 		the dchar represents the replaced content array index
75 		replaced content needs to tell the layouter: ascent, descent, width.
76 		all replaced content gets its own special segment.
77 		replaced content must be registered and const? or at the very least not modify things the layouter cares about. but better if nothing changes for undo sake.
78 
79 		it has a style but it only cares about the alignment from it.
80 +/
81 
82 /+
83 	HTML
84 		generally take all the text nodes and make them have unique text style instances
85 		the text style can then refer back to the dom for click handling, css forwarding etc.
86 
87 		but html has blocks...
88 
89 	BLOCK ELEMENTS
90 
91 		margin+padding behavior
92 		bounding box of nested things for background images and borders
93 
94 		an inline-block gets this stuff but does not go on its own line.
95 
96 	INLINE TABLES
97 +/
98 
99 // FIXME: add center, left, right, justify and valign top, bottom, middle, baseline
100 // valign top = ascent = 0 of line. bottom = descent = bottom of line. middle = ascent+descent/2 = middle of line. baseline = matched baselines
101 
102 // draw underline and strike through line segments - the offets may be in the font and underline might not want to slice the bottom fo p etc
103 // drawble textm ight give the offsets into the slice after all, and/or give non-trabable character things
104 
105 
106 // You can do the caret by any time it gets drawn, you set the flag that it is on, then you can xor it to turn it off and keep track of that at top level.
107 
108 
109 /++
110 	Represents the style of a span of text.
111 
112 	You should never mutate one of these, instead construct a new one.
113 
114 	Please note that methods may be added to this interface without being a full breaking change.
115 +/
116 interface TextStyle {
117 	/++
118 		Must never return `null`.
119 	+/
120 	MeasurableFont font();
121 
122 	// FIXME: I might also want a duplicate function for saving state.
123 
124 	// verticalAlign?
125 
126 	// i should keep a refcount here, then i can do a COW if i wanted to.
127 
128 	// you might use different style things to represent different  html elements or something too for click responses.
129 
130 	/++
131 		You can mix this in to your implementation class to get default implementations of new methods I add.
132 
133 		You will almost certainly want to override the things anyway, but this can help you keep things compiling.
134 
135 		Please note that there is no default for font.
136 	+/
137 	static mixin template Defaults() {
138 		/++
139 			The default returns a [TerminalFontRepresentation]. This is almost certainly NOT what you want,
140 			so implement your own `font()` member anyway.
141 		+/
142 		MeasurableFont font() {
143 			return TerminalFontRepresentation.instance;
144 		}
145 
146 	}
147 }
148 
149 /++
150 	This is a demo implementation of [MeasurableFont]. The expectation is more often that you'd use a [arsd.simpledisplay.OperatingSystemFont], which also implements this interface, but if you wanted to do your own thing this basic demo might help.
151 +/
152 class TerminalFontRepresentation : MeasurableFont {
153 	static TerminalFontRepresentation instance() {
154 		static TerminalFontRepresentation i;
155 		if(i is null)
156 			i = new TerminalFontRepresentation();
157 		return i;
158 	}
159 
160 	bool isMonospace() { return true; }
161 	int averageWidth() { return 1; }
162 	int height() { return 1; }
163 	/// since it is a grid this is a bit bizarre to translate.
164 	int ascent() { return 1; }
165 	int descent() { return 0; }
166 
167 	int stringWidth(scope const(char)[] s, SimpleWindow window = null) {
168 		int count;
169 		foreach(dchar ch; s)
170 			count++;
171 		return count;
172 	}
173 }
174 
175 /++
176 	A selection has four pieces:
177 
178 	1) A position
179 	2) An anchor
180 	3) A focus
181 	4) A user coordinate
182 
183 	The user coordinate should only ever be changed in direct response to actual user action and indicates
184 	where they ideally want the focus to be.
185 
186 	If they move in the horizontal direction, the x user coordinate should change. The y should not, even if the actual focus moved around (e.g. moving to a previous line while left arrowing).
187 
188 	If they move in a vertical direction, the y user coordinate should change. The x should not even if the actual focus moved around (e.g. going to the end of a shorter line while up arrowing).
189 
190 	The position, anchor, and focus are stored in opaque units. The user coordinate is EITHER grid coordinates (line, glyph) or screen coordinates (pixels).
191 
192 	Most methods on the selection move the position. This is not visible to the user, it is just an internal marker.
193 
194 	setAnchor() sets the anchor to the current position.
195 	setFocus() sets the focus to the current position.
196 
197 	The anchor is the part of the selection that doesn't move as you drag. The focus is the part of the selection that holds the caret and would move as you dragged around. (Open a program like Notepad and click and drag around. Your first click set the anchor, then as you drag, the focus moves around. The selection is everything between the anchor and the focus.)
198 
199 	The selection, while being fairly opaque, lets you do a great many things. Consider, for example, vim's 5dd command - delete five lines from the current position. You can do this by taking a selection, going to the beginning of the current line. Then dropping anchor. Then go down five lines and go to end of line. Then extend through the EOL character. Now delete the selection. Finally, restore the anchor and focus from the user coordinate, so their cursor on screen remains in the same approximate position.
200 
201 	The code can look something like this:
202 
203 	---
204 	selection
205 		.moveHome
206 		.setAnchor
207 		.moveDown(5)
208 		.moveEnd
209 		.moveForward(&isEol)
210 		.setFocus
211 		.deleteContent
212 		.moveToUserCoordinate
213 		.setAnchor;
214 	---
215 
216 	If you can think about how you'd do it on the standard keyboard, you can do it with this api. Everything between a setAnchor and setFocus would be like holding shift while doing the other things.
217 
218 	void selectBetween(Selection other);
219 
220 	Please note that this is just a handle to another object. Treat it as a reference type.
221 +/
222 public struct Selection {
223 	/++
224 		You cannot construct these yourself. Instead, use [TextLayouter.selection] to get it.
225 	+/
226 	@disable this();
227 	private this(TextLayouter layouter, int selectionId) {
228 		this.layouter = layouter;
229 		this.selectionId = selectionId;
230 	}
231 	private TextLayouter layouter;
232 	private int selectionId;
233 
234 	private ref SelectionImpl impl() {
235 		return layouter._selections[selectionId];
236 	}
237 
238 	/+ Inspection +/
239 
240 	/++
241 		Returns `true` if the selection is currently empty. An empty selection still has a position - where the cursor is drawn - but has no text inside it.
242 
243 		See_Also:
244 			[getContent], [getContentString]
245 	+/
246 	bool isEmpty() {
247 		return impl.focus == impl.anchor;
248 	}
249 
250 	/++
251 		Function to get the content of the selection. It is fed to you as a series of zero or more chunks of text and style information.
252 
253 		Please note that some text blocks may be empty, indicating only style has changed.
254 
255 		See_Also:
256 			[getContentString], [isEmpty]
257 	+/
258 	void getContent(scope void delegate(scope const(char)[] text, TextStyle style) dg) {
259 		dg(layouter.text[impl.start .. impl.end], null); // FIXME: style
260 	}
261 
262 	/++
263 		Convenience function to get the content of the selection as a simple string.
264 
265 		See_Also:
266 			[getContent], [isEmpty]
267 	+/
268 	string getContentString() {
269 		string s;
270 		getContent((txt, style) {
271 			s ~= txt;
272 		});
273 		return s;
274 	}
275 
276 	// need this so you can scroll found text into view and similar
277 	Rectangle focusBoundingBox() {
278 		return layouter.boundingBoxOfGlyph(layouter.findContainingSegment(impl.focus), impl.focus);
279 	}
280 
281 	/+ Setting the explicit positions to the current internal position +/
282 
283 	/++
284 		These functions set the actual selection from the current internal position.
285 
286 		A selection has two major pieces, the anchor and the focus, and a third bookkeeping coordinate, called the user coordinate.
287 
288 		It is best to think about these by thinking about the user interface. When you click and drag in a text document, the point where
289 		you clicked is the anchor position. As you drag, it moves the focus position. The selection is all the text between the anchor and
290 		focus. The cursor (also known as the caret) is drawn at the focus point.
291 
292 		Meanwhile, the user coordinate is the point where the user last explicitly moved the focus. Try clicking near the end of a long line,
293 		then moving up past a short line, to another long line. Your cursor should remain near the column of the original click, even though
294 		the focus moved left while passing through the short line. The user coordinate is how this is achieved - explicit user action on the
295 		horizontal axis (like pressing the left or right arrows) sets the X coordinate with [setUserXCoordinate], and explicit user action on the vertical axis sets the Y coordinate (like the up or down arrows) with [setUserYCoordinate], leaving X alone even if the focus moved horizontally due to a shorter or longer line. They're only moved together if the user action worked on both axes together (like a mouse click) with the [setUserCoordinate] function. Note that `setUserCoordinate` remembers the column even if there is no glyph there, making it ideal for mouse interaction, whereas the `setUserXCoordinate` and `setUserYCoordinate` set it to the position of the glyph on the focus, making them more suitable for keyboard interaction.
296 
297 		Before you set one of these values, you move the internal position with the `move` family of functions ([moveTo], [moveLeft], etc.).
298 
299 		Setting the anchor also always sets the focus.
300 
301 		For example, to select the whole document:
302 
303 		---
304 		with(selection) {
305 			moveToStartOfDocument(); // changes internal position without affecting the actual selection
306 			setAnchor(); // set the anchor, actually changing the selection.
307 			// Note that setting the anchor also always sets the focus, so the selection is empty at this time.
308 			moveToEndOfDocument(); // move the internal position to the end
309 			setFocus(); // and now set the focus, which extends the selection from the anchor, meaning the whole document is selected now
310 		}
311 		---
312 
313 		I didn't set the user coordinate there since the user's action didn't specify a row or column.
314 	+/
315 	Selection setAnchor() {
316 		impl.anchor = impl.position;
317 		impl.focus = impl.position;
318 		return this;
319 	}
320 
321 	/// ditto
322 	Selection setFocus() {
323 		impl.focus = impl.position;
324 		return this;
325 	}
326 
327 	/// ditto
328 	Selection setUserCoordinate(Point p) {
329 		impl.virtualFocusPosition = p;
330 		return this;
331 	}
332 
333 	/// ditto
334 	Selection setUserXCoordinate() {
335 		impl.virtualFocusPosition.x = layouter.boundingBoxOfGlyph(layouter.findContainingSegment(impl.position), impl.position).left;
336 		return this;
337 	}
338 
339 	/// ditto
340 	Selection setUserYCoordinate() {
341 		impl.virtualFocusPosition.y = layouter.boundingBoxOfGlyph(layouter.findContainingSegment(impl.position), impl.position).top;
342 		return this;
343 	}
344 
345 
346 	/+ Moving the internal position +/
347 
348 	/++
349 
350 	+/
351 	Selection moveTo(Point p, bool setUserCoordinate = true) {
352 		impl.position = layouter.offsetOfClick(p);
353 		if(setUserCoordinate)
354 			impl.virtualFocusPosition = p;
355 		return this;
356 	}
357 
358 	/++
359 
360 	+/
361 	Selection moveToStartOfDocument() {
362 		impl.position = 0;
363 		return this;
364 	}
365 
366 	/// ditto
367 	Selection moveToEndOfDocument() {
368 		impl.position = cast(int) layouter.text.length - 1; // never include the 0 terminator
369 		return this;
370 	}
371 
372 	/++
373 
374 	+/
375 	Selection moveToStartOfLine(bool byRender = true, bool includeLeadingWhitespace = true) {
376 		// FIXME: chekc for word wrap by checking segment.displayLineNumber
377 		// FIXME: includeLeadingWhitespace
378 		while(impl.position > 0 && layouter.text[impl.position - 1] != '\n')
379 			impl.position--;
380 
381 		return this;
382 	}
383 
384 	/// ditto
385 	Selection moveToEndOfLine(bool byRender = true) {
386 		// FIXME: chekc for word wrap by checking segment.displayLineNumber
387 		while(impl.position + 1 < layouter.text.length && layouter.text[impl.position] != '\n') // never include the 0 terminator
388 			impl.position++;
389 		return this;
390 	}
391 
392 	/++
393 		If the position is abutting an end of line marker, it moves past it, to include it.
394 		If not, it does nothing.
395 
396 		The intention is so you can delete a whole line by doing:
397 
398 		---
399 		with(selection) {
400 			moveToStartOfLine();
401 			setAnchor();
402 			// this moves to the end of the visible line, but if you stopped here, you'd be left with an empty line
403 			moveToEndOfLine();
404 			// this moves past the line marker, meaning you don't just delete the line's content, it deletes the entire line
405 			moveToIncludeAdjacentEndOfLineMarker();
406 			setFocus();
407 			replaceContent("");
408 		}
409 		---
410 	+/
411 	Selection moveToIncludeAdjacentEndOfLineMarker() {
412 		// FIXME: i need to decide what i want to do about \r too. Prolly should remove it at the boundaries.
413 		if(impl.position + 1 < layouter.text.length && layouter.text[impl.position] == '\n') { // never include the 0 terminator
414 			impl.position++;
415 		}
416 		return this;
417 	}
418 
419 	// note there's move up / down / left / right
420 	// in addition to move forward / backward glyph/line
421 	// the directions always match what's on screen.
422 	// the others always match the logical order in the string.
423 	/++
424 
425 	+/
426 	Selection moveUp(int count = 1, bool byRender = true) {
427 		verticalMoveImpl(-1, count, byRender);
428 		return this;
429 	}
430 
431 	/// ditto
432 	Selection moveDown(int count = 1, bool byRender = true) {
433 		verticalMoveImpl(1, count, byRender);
434 		return this;
435 	}
436 
437 	/// ditto
438 	Selection moveLeft(int count = 1, bool byRender = true) {
439 		horizontalMoveImpl(-1, count, byRender);
440 		return this;
441 	}
442 
443 	/// ditto
444 	Selection moveRight(int count = 1, bool byRender = true) {
445 		horizontalMoveImpl(1, count, byRender);
446 		return this;
447 	}
448 
449 	void find(scope const(char)[] text) {
450 	}
451 
452 	/+ +
453 		Searches by regex.
454 
455 		This is a template because the regex engine can be a heavy dependency, so it is only
456 		included if you need it. The RegEx object is expected to match the Phobos std.regex.RegEx
457 		api, so while you could, in theory, replace it, it is probably easier to just use the Phobos one.
458 	+/
459 	void find(RegEx)(RegEx re) {
460 
461 	}
462 
463 	/+ Manipulating the data in the selection +/
464 
465 	/++
466 		Replaces the content of the selection. If you replace it with an empty `newText`, it will delete the content.
467 
468 		If newText == "\b", it will delete the selection if it is non-empty, and otherwise delete the thing before the cursor.
469 
470 		If you want to do normal editor backspace key, you might want to check `if(!selection.isEmpty()) selection.moveLeft();`
471 		before calling `selection.deleteContent()`. Similarly, for the delete key, you might use `moveRight` instead, since this
472 		function will do nothing for an empty selection by itself.
473 
474 		FIXME: what if i want to replace it with some multiply styled text? Could probably call it in sequence actually.
475 	+/
476 	Selection replaceContent(scope const(char)[] newText, TextLayouter.StyleHandle style = TextLayouter.StyleHandle.init) {
477 		layouter.wasMutated_ = true;
478 
479 		if(style == TextLayouter.StyleHandle.init)
480 			style = layouter.getInsertionStyleAt(impl.focus);
481 
482 		int removeBegin, removeEnd;
483 		if(this.isEmpty()) {
484 			if(newText.length == 1 && newText[0] == '\b') {
485 				auto place = impl.focus;
486 				if(place > 0) {
487 					int amount = 1;
488 					while((layouter.text[place - amount] & 0b11000000) == 0b10000000) // all non-start bytes of a utf-8 sequence have this convenient property
489 						amount++; // assumes this will never go over the edge cuz of it being valid utf 8 internally
490 
491 					removeBegin = place - amount;
492 					removeEnd = place;
493 
494 					if(removeBegin < 0)
495 						removeBegin = 0;
496 					if(removeEnd < 0)
497 						removeEnd = 0;
498 				}
499 
500 				newText = null;
501 			} else {
502 				removeBegin = impl.terminus;
503 				removeEnd = impl.terminus;
504 			}
505 		} else {
506 			removeBegin = impl.start;
507 			removeEnd = impl.end;
508 			if(newText.length == 1 && newText[0] == '\b') {
509 				newText = null;
510 			}
511 		}
512 
513 		auto place = impl.terminus;
514 
515 		auto changeInLength = cast(int) newText.length - (removeEnd - removeBegin);
516 
517 		// FIXME: the horror
518 		auto trash = layouter.text[0 .. removeBegin];
519 		trash ~= newText;
520 		trash ~= layouter.text[removeEnd .. $];
521 		layouter.text = trash;
522 
523 		impl.position = removeBegin + cast(int) newText.length;
524 		this.setAnchor();
525 
526 		/+
527 			For styles:
528 				if one part resides in the deleted zone, it should be truncated to the edge of the deleted zone
529 				if they are entirely in the deleted zone - their new length is zero - they should simply be deleted
530 				if they are entirely before the deleted zone, it can stay the same
531 				if they are entirely after the deleted zone, they should get += changeInLength
532 
533 				FIXME: if the deleted zone lies entirely inside one of the styles, that style's length should be extended to include the new text if it has no style, or otherwise split into a few style blocks
534 
535 				However, the algorithm for default style in the new zone is a bit different: if at index 0 or right after a \n, it uses the next style. otherwise it uses the previous one.
536 		+/
537 
538 		//writeln(removeBegin, " ", removeEnd);
539 		//foreach(st; layouter.styles) writeln("B: ", st.offset, "..", st.offset + st.length, " ", st.styleInformationIndex);
540 
541 		// first I'm going to update all of them so it is in a consistent state
542 		foreach(ref st; layouter.styles) {
543 			auto begin = st.offset;
544 			auto end = st.offset + st.length;
545 
546 			void adjust(ref int what) {
547 				if(what < removeBegin) {
548 					// no change needed
549 				} else if(what >= removeBegin && what < removeEnd) {
550 					what = removeBegin;
551 				} else if(what) {
552 					what += changeInLength;
553 				}
554 			}
555 
556 			adjust(begin);
557 			adjust(end);
558 
559 			assert(end >= begin); // empty styles are not permitted by the implementation
560 			st.offset = begin;
561 			st.length = end - begin;
562 		}
563 
564 		// then go back and inject the new style, if needed
565 		if(changeInLength > 0) {
566 			changeStyle(removeBegin, removeBegin + cast(int) newText.length, style);
567 		}
568 
569 		removeEmptyStyles();
570 
571 		// or do i want to use init to just keep using the same style as is already there?
572 		// FIXME
573 		//if(style !is StyleHandle.init) {
574 			// styles ~= StyleBlock(cast(int) before.length, cast(int) changeInLength, style.index);
575 		//}
576 
577 
578 		auto endInvalidate = removeBegin + newText.length;
579 		if(removeEnd > endInvalidate)
580 			endInvalidate = removeEnd;
581 		layouter.invalidateLayout(removeBegin, endInvalidate, changeInLength);
582 
583 		// there's a new style from removeBegin to removeBegin + newText.length
584 
585 		// FIXME other selections in the zone need to be adjusted too
586 		// if they are in the deleted zone, it should be moved to the end of the new zone (removeBegin + newText.length)
587 		// if they are before the deleted zone, they can stay the same
588 		// if they are after the deleted zone, they should be adjusted by changeInLength
589 		foreach(idx, ref selection; layouter._selections[0 .. layouter.selectionsInUse]) {
590 
591 			// don't adjust ourselves here, we already did it above
592 			// and besides don't want mutation in here
593 			if(idx == selectionId)
594 				continue;
595 
596 			void adjust(ref int what) {
597 				if(what < removeBegin) {
598 					// no change needed
599 				} else if(what >= removeBegin && what < removeEnd) {
600 					what = removeBegin;
601 				} else if(what) {
602 					what += changeInLength;
603 				}
604 			}
605 
606 			adjust(selection.anchor);
607 			adjust(selection.terminus);
608 		}
609 			// you might need to set the user coordinate after this!
610 
611 		return this;
612 	}
613 
614 	private void removeEmptyStyles() {
615 		/+ the code doesn't like empty style blocks, so gonna go back and remove those +/
616 		for(int i = 0; i < cast(int) layouter.styles.length; i++) {
617 			if(layouter.styles[i].length == 0) {
618 				for(auto i2 = i; i2 + 1 < layouter.styles.length; i2++)
619 					layouter.styles[i2] = layouter.styles[i2 + 1];
620 				layouter.styles = layouter.styles[0 .. $-1];
621 				layouter.styles.assumeSafeAppend();
622 				i--;
623 			}
624 		}
625 	}
626 
627 	/++
628 		Changes the style of the given selection. Gives existing styles in the selection to your delegate
629 		and you return a new style to assign to that block.
630 	+/
631 	public void changeStyle(TextLayouter.StyleHandle delegate(TextStyle existing) newStyle) {
632 		// FIXME there might be different sub-styles so we should actually look them up and send each one
633 		auto ns = newStyle(null);
634 		changeStyle(impl.start, impl.end, ns);
635 		removeEmptyStyles();
636 
637 		layouter.invalidateLayout(impl.start, impl.end, 0);
638 	}
639 
640 	/+ Impl helpers +/
641 
642 	private void changeStyle(int newStyleBegin, int newStyleEnd, TextLayouter.StyleHandle style) {
643 		// FIXME: binary search
644 		for(size_t i = 0; i < layouter.styles.length; i++) {
645 			auto s = &layouter.styles[i];
646 			const oldStyleBegin = s.offset;
647 			const oldStyleEnd = s.offset + s.length;
648 
649 			if(newStyleBegin >= oldStyleBegin && newStyleBegin < oldStyleEnd) {
650 				// the cases:
651 
652 				// it is an exact match in size, we can simply overwrite it
653 				if(newStyleBegin == oldStyleBegin && newStyleEnd == oldStyleEnd) {
654 					s.styleInformationIndex = style.index;
655 					break; // all done
656 				}
657 				// we're the same as the existing style, so it is just a matter of extending it to include us
658 				else if(s.styleInformationIndex == style.index) {
659 					if(newStyleEnd > oldStyleEnd) {
660 						s.length = newStyleEnd - oldStyleBegin;
661 
662 						// then need to fix up all the subsequent blocks, adding the offset, reducing the length
663 						int remainingFixes = newStyleEnd - oldStyleEnd;
664 						foreach(st; layouter.styles[i + 1 .. $]) {
665 							auto thisFixup = remainingFixes;
666 							if(st.length < thisFixup)
667 								thisFixup = st.length;
668 							// this can result in 0 length, the loop after this will delete that.
669 							st.offset += thisFixup;
670 							st.length -= thisFixup;
671 
672 							remainingFixes -= thisFixup;
673 
674 							assert(remainingFixes >= 0);
675 
676 							if(remainingFixes == 0)
677 								break;
678 						}
679 					}
680 					// otherwise it is all already there and nothing need be done at all
681 					break;
682 				}
683 				// for the rest of the cases, the style does not match and is not a size match,
684 				// so a new block is going to have to be inserted
685 				// ///////////
686 				// we're entirely contained inside, so keep the left, insert ourselves, and re-create right.
687 				else if(newStyleEnd > oldStyleBegin && newStyleEnd < oldStyleEnd) {
688 					// keep the old style on the left...
689 					s.length = newStyleBegin - oldStyleBegin;
690 
691 					auto toInsert1 = TextLayouter.StyleBlock(newStyleBegin, newStyleEnd - newStyleBegin, style.index);
692 					auto toInsert2 = TextLayouter.StyleBlock(newStyleEnd, oldStyleEnd - newStyleEnd, s.styleInformationIndex);
693 
694 					layouter.styles = layouter.styles[0 .. i + 1] ~ toInsert1 ~ toInsert2 ~ layouter.styles[i + 1 .. $];
695 
696 					// writeln(*s); writeln(toInsert1); writeln(toInsert2);
697 
698 					break; // no need to continue processing as the other offsets are unaffected
699 				}
700 				// we need to keep the left end of the original thing, but then insert ourselves on afterward
701 				else if(newStyleBegin >= oldStyleBegin) {
702 					// want to just shorten the original thing, then adjust the values
703 					// so next time through the loop can work on that existing block
704 
705 					s.length = newStyleBegin - oldStyleBegin;
706 
707 					// extend the following style to start here, so there's no gap in the next loop iteration
708 					if(i + i < layouter.styles.length) {
709 						auto originalOffset = layouter.styles[i+1].offset;
710 						assert(originalOffset >= newStyleBegin);
711 						layouter.styles[i+1].offset = newStyleBegin;
712 						layouter.styles[i+1].length += originalOffset - newStyleBegin;
713 
714 						// i will NOT change the style info index yet, since the next iteration will do that
715 						continue;
716 					} else {
717 						// at the end of the loop we can just append the new thing and break out of here
718 						layouter.styles ~= TextLayouter.StyleBlock(newStyleBegin, newStyleEnd - newStyleBegin, style.index);
719 						break;
720 					}
721 				}
722 				else {
723 					// this should be impossible as i think i covered all the cases above
724 					// as we iterate through
725 					// writeln(oldStyleBegin, "..", oldStyleEnd, " -- ", newStyleBegin, "..", newStyleEnd);
726 					assert(0);
727 				}
728 			}
729 		}
730 
731 		// foreach(st; layouter.styles) writeln("A: ", st.offset, "..", st.offset + st.length, " ", st.styleInformationIndex);
732 	}
733 
734 	// returns the edge of the new cursor position
735 	private void horizontalMoveImpl(int direction, int count, bool byRender) {
736 		assert(direction != 0);
737 
738 		auto place = impl.focus + direction;
739 
740 		foreach(c; 0 .. count) {
741 			while(place >= 0 && place < layouter.text.length && (layouter.text[place] & 0b11000000) == 0b10000000) // all non-start bytes of a utf-8 sequence have this convenient property
742 				place += direction;
743 		}
744 
745 		// FIXME if(byRender), if we're on a rtl line, swap the things. but if it is mixed it won't even do anything and stay in logical order
746 
747 		if(place < 0)
748 			place = 0;
749 		if(place >= layouter.text.length)
750 			place = cast(int) layouter.text.length - 1;
751 
752 		impl.position = place;
753 
754 	}
755 
756 	// returns the baseline of the new cursor
757 	private void verticalMoveImpl(int direction, int count, bool byRender) {
758 		assert(direction != 0);
759 		// this needs to find the closest glyph on the virtual x on the previous (rendered) line
760 
761 		int segmentIndex = layouter.findContainingSegment(impl.terminus);
762 
763 		// we know this is going to lead to a different segment since
764 		// the layout breaks up that way, so we can just go straight backward
765 
766 		auto segment = layouter.segments[segmentIndex];
767 
768 		auto idealX = impl.virtualFocusPosition.x;
769 
770 		auto targetLineNumber = segment.displayLineNumber + (direction * count);
771 		if(targetLineNumber < 0)
772 			targetLineNumber = 0;
773 
774 		// FIXME if(!byRender)
775 
776 
777 		// FIXME: when you are going down, a line that begins with tab doesn't highlight the right char.
778 
779 		int bestHit = -1;
780 		int bestHitDistance = int.max;
781 
782 		// writeln(targetLineNumber, " ", segmentIndex, " ", layouter.segments.length);
783 
784 		segmentLoop: while(segmentIndex >= 0 && segmentIndex < layouter.segments.length) {
785 			segment = layouter.segments[segmentIndex];
786 			if(segment.displayLineNumber == targetLineNumber) {
787 				// we're in the right line... but not necessarily the right segment
788 				// writeln("line found");
789 				if(idealX >= segment.boundingBox.left && idealX < segment.boundingBox.right) {
790 					// need to find the exact thing in here
791 
792 					auto hit = segment.textBeginOffset;
793 					auto ul = segment.upperLeft;
794 
795 					bool found;
796 					auto txt = layouter.text[segment.textBeginOffset .. segment.textEndOffset];
797 					auto codepoint = 0;
798 					foreach(idx, dchar d; txt) {
799 						auto width = layouter.segmentsWidths[segmentIndex][codepoint];
800 
801 						hit = segment.textBeginOffset + cast(int) idx;
802 
803 						auto distanceToLeft = ul.x - idealX;
804 						if(distanceToLeft < 0) distanceToLeft = -distanceToLeft;
805 						if(distanceToLeft < bestHitDistance) {
806 							bestHit = hit;
807 							bestHitDistance = distanceToLeft;
808 						} else {
809 							// getting further away = no help
810 							break;
811 						}
812 
813 						/*
814 						// FIXME: I probably want something slightly different
815 						if(ul.x >= idealX) {
816 							found = true;
817 							break;
818 						}
819 						*/
820 
821 						ul.x += width;
822 						codepoint++;
823 					}
824 
825 					/*
826 					if(!found)
827 						hit = segment.textEndOffset - 1;
828 
829 					impl.position = hit;
830 					bestHit = -1;
831 					*/
832 
833 					impl.position = bestHit;
834 					bestHit = -1;
835 
836 					// selections[selectionId].virtualFocusPosition = Point(selections[selectionId].virtualFocusPosition.x, segment.boundingBox.bottom);
837 
838 					break segmentLoop;
839 				} else {
840 					// FIXME: assuming ltr here
841 					auto distance = idealX - segment.boundingBox.right;
842 					if(distance < 0)
843 						distance = -distance;
844 					if(bestHit == -1 || distance < bestHitDistance) {
845 						bestHit = segment.textEndOffset - 1;
846 						bestHitDistance = distance;
847 					}
848 				}
849 			} else if(bestHit != -1) {
850 				impl.position = bestHit;
851 				bestHit = -1;
852 				break segmentLoop;
853 			}
854 
855 			segmentIndex += direction;
856 		}
857 
858 		if(bestHit != -1)
859 			impl.position = bestHit;
860 
861 		if(impl.position == layouter.text.length)
862 			impl.position -- ; // never select the eof marker
863 	}
864 }
865 
866 private struct SelectionImpl {
867 	// you want multiple selections at most points
868 	int id;
869 	int anchor;
870 	int terminus;
871 
872 	int position;
873 
874 	alias focus = terminus;
875 
876 	/+
877 		As you move through lines of different lengths, your actual x will change,
878 		but the user will want to stay in the same relative spot, consider passing:
879 
880 		long thing
881 		short
882 		long thing
883 
884 		from the 'i'. When you go down, you'd be back by the t, but go down again, you should
885 		go back to the i. This variable helps achieve this.
886 	+/
887 	Point virtualFocusPosition;
888 
889 	int start() {
890 		return anchor <= terminus ? anchor : terminus;
891 	}
892 	int end() {
893 		return anchor <= terminus ? terminus : anchor;
894 	}
895 	bool empty() {
896 		return anchor == terminus;
897 	}
898 	bool containsOffset(int textOffset) {
899 		return textOffset >= start && textOffset < end;
900 	}
901 	bool isIncludedInRange(int textStart, int textEnd) {
902 		// if either end are in there, we're obviously in the range
903 		if((start >= textStart && start < textEnd) || (end >= textStart && end < textEnd))
904 			return true;
905 		// or if the selection is entirely inside the given range...
906 		if(start >= textStart && end < textEnd)
907 			return true;
908 		// or if the given range is at all inside the selection
909 		if((textStart >= start && textStart < end) || (textEnd >= start && textEnd < end))
910 			return true;
911 		return false;
912 	}
913 }
914 
915 /++
916 	Bugs:
917 		Only tested on Latin scripts at this time. Other things should be possible but might need work. Let me know if you need it and I'll see what I can do.
918 +/
919 class TextLayouter {
920 
921 
922 	// actually running this invariant gives quadratic performance in the layouter (cuz of isWordwrapPoint lol)
923 	// so gonna only version it in for special occasions
924 	version(none)
925 	invariant() {
926 		// There is one and exactly one segment for every char in the string.
927 		// The segments are stored in sequence from index 0 to the end of the string.
928 		// styleInformationIndex is always in bounds of the styles array.
929 		// There is one and exactly one style block for every char in the string.
930 		// Style blocks are stored in sequence from index 0 to the end of the string.
931 
932 		assert(text.length > 0 && text[$-1] == 0);
933 		assert(styles.length >= 1);
934 		int last = 0;
935 		foreach(style; styles) {
936 			assert(style.offset == last); // all styles must be in order and contiguous
937 			assert(style.length > 0); // and non-empty
938 			assert(style.styleInformationIndex != -1); // and not default constructed (this should be resolved before adding)
939 			assert(style.styleInformationIndex >= 0 && style.styleInformationIndex < stylePalette.length); // all must be in-bounds
940 			last = style.offset + style.length;
941 		}
942 		assert(last == text.length); // and all chars in the array must be covered by a style block
943 	}
944 
945 	/++
946 		Gets the object representing the given selection.
947 
948 		Normally, id = 0 is the user's selection, then id's 60, 61, 62, and 63 are private to the application.
949 	+/
950 	Selection selection(int id = 0) {
951 		assert(id >= 0 && id < _selections.length);
952 		return Selection(this, id);
953 	}
954 
955 	/++
956 		The rendered size of the text.
957 	+/
958 	public int width() {
959 		relayoutIfNecessary();
960 		return _width;
961 	}
962 
963 	/// ditto
964 	public int height() {
965 		relayoutIfNecessary();
966 		return _height;
967 	}
968 
969 	static struct State {
970 		// for the delta compression, the text is the main field to worry about
971 		// and what it really needs to know is just based on what, then what is added and what is removed.
972 		// i think everything else i'd just copy in (or reference the same array) anyway since they're so
973 		// much smaller anyway.
974 		//
975 		// and if the text is small might as well just copy/reference it too tbh.
976 		private {
977 			char[] text;
978 			TextStyle[] stylePalette;
979 			StyleBlock[] styles;
980 			SelectionImpl[] selections;
981 		}
982 	}
983 
984 	// for manual undo stuff
985 	// and all state should be able to do do it incrementally too; each modification to those should be compared.
986 	/++
987 		The editor's internal state can be saved and restored as an opaque blob. You might use this to make undo checkpoints and similar.
988 
989 		Its implementation may use delta compression from a previous saved state, it will try to do this transparently for you to save memory.
990 	+/
991 	const(State)* saveState() {
992 		return new State(text.dup, stylePalette.dup, styles.dup, _selections.dup);
993 	}
994 	/// ditto
995 	void restoreState(const(State)* state) {
996 		auto changeInLength = cast(int) this.text.length - cast(int) state.text.length;
997 		this.text = state.text.dup;
998 		// FIXME: bad cast
999 		this.stylePalette = (cast(TextStyle[]) state.stylePalette).dup;
1000 		this.styles = state.styles.dup;
1001 		this._selections = state.selections.dup;
1002 
1003 		invalidateLayout(0, text.length, changeInLength);
1004 	}
1005 
1006 	// FIXME: I might want to make the original line number exposed somewhere too like in the segment draw information
1007 
1008 	// FIXME: all the actual content - styles, text, and selection stuff - needs to be able to communicate its changes
1009 	// incrementally for the network use case. the segments tho not that important.
1010 
1011 	// FIXME: for the password thing all the glyph positions need to be known to this system, so it can't just draw it
1012 	// that way (unless it knows it is using a monospace font... but we can trick it by giving it a fake font that gives all those metrics)
1013 	// so actually that is the magic lol
1014 
1015 	private static struct StyleBlock {
1016 		int offset;
1017 		int length;
1018 
1019 		int styleInformationIndex;
1020 	}
1021 
1022 	void resetSelection(int selectionId) {
1023 
1024 	}
1025 
1026 	// FIXME: is it moving teh anchor or the focus?
1027 	void extendSelection(int selectionId, bool fromBeginning, bool direction, int delegate(scope const char[] c) handler) {
1028 		// iterates through the selection, giving you the chars, until you return 0
1029 		// can use this to do things like delete words in front of cursor
1030 	}
1031 
1032 	void duplicateSelection(int receivingSelectionId, int sourceSelectionId) {
1033 
1034 	}
1035 
1036 	private int findContainingSegment(int textOffset) {
1037 
1038 		relayoutIfNecessary();
1039 
1040 		// FIXME: binary search
1041 
1042 		// FIXME: when the index is here, use it
1043 		foreach(idx, segment; segments) {
1044 			// this assumes the segments are in order of text offset
1045 			if(textOffset >= segment.textBeginOffset && textOffset < segment.textEndOffset)
1046 				return cast(int) idx;
1047 		}
1048 		assert(0);
1049 	}
1050 
1051 	// need page up+down, home, edit, arrows, etc.
1052 
1053 	/++
1054 		Finds the given text, setting the given selection to it, if found.
1055 
1056 		Starts from the given selection and moves in the direction to find next.
1057 
1058 		Returns true if found.
1059 	+/
1060 	FindResult find(int selectionId, in const(char)[] text, bool direction, bool wraparound) {
1061 		return FindResult.NotFound;
1062 	}
1063 	/// ditto
1064 	enum FindResult : int {
1065 		NotFound = 0,
1066 		Found = 1,
1067 		WrappedAround = 2
1068 	}
1069 
1070 	private bool wasMutated_ = false;
1071 	/++
1072 		The layouter maintains a flag to tell if the content has been changed.
1073 	+/
1074 	public bool wasMutated() {
1075 		return wasMutated_;
1076 	}
1077 
1078 	/// ditto
1079 	public void clearWasMutatedFlag() {
1080 		wasMutated_ = false;
1081 	}
1082 
1083 	/++
1084 		Represents a possible registered style for a segment of text.
1085 	+/
1086 	public static struct StyleHandle {
1087 		private this(int idx) { this.index = idx; }
1088 		private int index = -1;
1089 	}
1090 
1091 	/++
1092 		Registers a text style you can use in text segments.
1093 	+/
1094 	// FIXME: i might have to construct it internally myself so i can return it const.
1095 	public StyleHandle registerStyle(TextStyle style) {
1096 		stylePalette ~= style;
1097 		return StyleHandle(cast(int) stylePalette.length - 1);
1098 	}
1099 
1100 
1101 	/++
1102 		Appends text at the end, without disturbing user selection.
1103 	+/
1104 	public void appendText(scope const(char)[] text, StyleHandle style = StyleHandle.init) {
1105 		wasMutated_ = true;
1106 		auto before = this.text;
1107 		this.text.length += text.length;
1108 		this.text[before.length-1 .. before.length-1 + text.length] = text[];
1109 		this.text[$-1] = 0; // gotta maintain the zero terminator i use
1110 		// or do i want to use init to just keep using the same style as is already there?
1111 		if(style is StyleHandle.init) {
1112 			// default is to extend the existing style
1113 			styles[$-1].length += text.length;
1114 		} else {
1115 			// otherwise, insert a new block for it
1116 			styles[$-1].length -= 1; // it no longer covers the zero terminator
1117 
1118 			// but this does, hence the +1
1119 			styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length + 1, style.index);
1120 		}
1121 
1122 		invalidateLayout(cast(int) before.length - 1 /* zero terminator */, this.text.length, cast(int) text.length);
1123 	}
1124 
1125 	/++
1126 		Calls your delegate for each segment of the text, guaranteeing you will be called exactly once for each non-nil char in the string and each slice will have exactly one style. A segment may be as small as a single char.
1127 
1128 		FIXME: have a getTextInSelection
1129 
1130 		FIXME: have some kind of index stuff so you can select some text found in here (think regex search)
1131 	+/
1132 	void getText(scope void delegate(scope const(char)[] segment, TextStyle style) handler) {
1133 		handler(text[0 .. $-1], null); // cut off the null terminator
1134 	}
1135 
1136 	/++
1137 		Gets the current text value as a plain-text string.
1138 	+/
1139 	string getTextString() {
1140 		string s;
1141 		getText((segment, style) {
1142 			s ~= segment;
1143 		});
1144 		return s;
1145 	}
1146 
1147 	public static struct DrawingInformation {
1148 		Rectangle boundingBox;
1149 		Point initialBaseline;
1150 		ulong selections; // 0 if not selected. bitmask of selection ids otherwise
1151 
1152 		int direction; // you start at initialBaseline then draw ltr or rtl or up or down.
1153 		// might also store glyph id, which could be encoded texture # + position, stuff like that. if each segment were
1154 		// a glyph at least which is sometimes needed but prolly not gonna stress abut that in my use cases, i'd rather batch.
1155 	}
1156 
1157 	public static struct CaretInformation {
1158 		int id;
1159 		Rectangle boundingBox;
1160 	}
1161 
1162 	// assumes the idx is indeed in the segment
1163 	private Rectangle boundingBoxOfGlyph(size_t segmentIndex, int idx) {
1164 		// I can't relayoutIfNecessary here because that might invalidate the segmentIndex!!
1165 		// relayoutIfNecessary();
1166 		auto segment = segments[segmentIndex];
1167 
1168 		int codepointCounter = 0;
1169 		auto bb = segment.boundingBox;
1170 		foreach(thing, dchar cp; text[segment.textBeginOffset .. segment.textEndOffset]) {
1171 			auto w = segmentsWidths[segmentIndex][codepointCounter];
1172 
1173 			if(thing + segment.textBeginOffset == idx) {
1174 				bb.right = bb.left + w;
1175 				return bb;
1176 			}
1177 
1178 			bb.left += w;
1179 
1180 			codepointCounter++;
1181 		}
1182 
1183 		bb.right = bb.left + 1;
1184 
1185 		return bb;
1186 	}
1187 
1188 	void getTextAtPosition(Point p) {
1189 		relayoutIfNecessary();
1190 		// return the text in that segment, the style info attached, and if that specific point is part of a selection (can be used to tell if it should be a drag operation)
1191 		// then might want dropTextAt(Point p)
1192 	}
1193 
1194 	/++
1195 		Gets the text that you need to draw, guaranteeing each call to your delegate will:
1196 
1197 		* Have a contiguous slice into text
1198 		* Have exactly one style (which may be null, meaning use all your default values. Be sure you draw with the same font you passed as the default font to TextLayouter.)
1199 		* Be a linear block of text that fits in a single rectangular segment
1200 		* A segment will be as large a block of text as the implementation can do, but it may be as short as a single char.
1201 		* The segment may be a special escape sequence. FIXME explain how to check this.
1202 
1203 		Return `false` from your delegate to stop iterating through the text.
1204 
1205 		Please note that the `caretPosition` can be `Rectangle.init`, indicating it is not present in this segment. If it is not that, it will be the bounding box of the glyph.
1206 
1207 		You can use the `startFrom` parameter to skip ahead. The intended use case for this is to start from a scrolling position in the box; the first segment given will include this point. FIXME: maybe it should just go ahead and do a bounding box. Note that the segments may extend outside the point; it is just meant that it will include that and try to trim the rest.
1208 
1209 		The segment may include all forms of whitespace, including newlines, tab characters, etc. Generally, a tab character will be in its own segment and \n will appear at the end of a segment. You will probably want to `stripRight` each segment depending on your drawing functions.
1210 	+/
1211 	void getDrawableText(scope bool delegate(scope const(char)[] segment, TextStyle style, DrawingInformation information, CaretInformation[] carets...) dg, Rectangle box = Rectangle.init) {
1212 		relayoutIfNecessary();
1213 		getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
1214 			if(segment.textBeginOffset == -1)
1215 				return true;
1216 
1217 			TextStyle style;
1218 			assert(segment.styleInformationIndex < stylePalette.length);
1219 
1220 			style = stylePalette[segment.styleInformationIndex];
1221 
1222 			ubyte[64] possibleSelections;
1223 			int possibleSelectionsCount;
1224 
1225 			CaretInformation[64] caretInformation;
1226 			int cic;
1227 
1228 			// bounding box reduction
1229 			foreach(si, selection; _selections[0 .. selectionsInUse]) {
1230 				if(selection.isIncludedInRange(segment.textBeginOffset, segment.textEndOffset)) {
1231 					if(!selection.empty()) {
1232 						possibleSelections[possibleSelectionsCount++] = cast(ubyte) si;
1233 					}
1234 					if(selection.focus >= segment.textBeginOffset && selection.focus < segment.textEndOffset) {
1235 
1236 						// make sure the caret box represents that it would be if we actually
1237 						// did the insertion, so adjust the bounding box to account for a possibly
1238 						// different font
1239 
1240 						auto insertionStyle = stylePalette[getInsertionStyleAt(selection.focus).index];
1241 						auto glyphStyle = style;
1242 
1243 						auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
1244 
1245 						bb.top += glyphStyle.font.ascent;
1246 						bb.bottom -= glyphStyle.font.descent;
1247 
1248 						bb.top -= insertionStyle.font.ascent;
1249 						bb.bottom += insertionStyle.font.descent;
1250 
1251 						caretInformation[cic++] = CaretInformation(cast(int) si, bb);
1252 					}
1253 				}
1254 			}
1255 
1256 			// the rest of this might need splitting based on selections
1257 
1258 			DrawingInformation di;
1259 			di.boundingBox = Rectangle(segment.upperLeft, Size(segment.width, segment.height));
1260 			di.selections = 0;
1261 
1262 			// di.initialBaseline = Point(x, y); // FIXME
1263 			// FIXME if the selection focus is in this box, we should set the caretPosition to the bounding box of the associated glyph
1264 			// di.caretPosition = Rectangle(x, y, w, h); // FIXME
1265 
1266 			auto end = segment.textEndOffset;
1267 			if(end == text.length)
1268 				end--; // don't send the terminating 0 to the user as that's an internal detail
1269 
1270 			auto txt = text[segment.textBeginOffset .. end];
1271 
1272 			if(possibleSelectionsCount == 0) {
1273 				// no selections present, no need to iterate
1274 				// FIXME: but i might have to take some gap chars and such out anyway.
1275 				return dg(txt, style, di, caretInformation[0 .. cic]);
1276 			} else {
1277 				ulong lastSel = 0;
1278 				size_t lastSelPos = 0;
1279 				size_t lastSelCodepoint = 0;
1280 				bool exit = false;
1281 
1282 				void sendSegment(size_t start, size_t end, size_t codepointStart, size_t codepointEnd) {
1283 					di.selections = lastSel;
1284 
1285 					Rectangle bbOriginal = di.boundingBox;
1286 
1287 					int segmentWidth;
1288 
1289 					foreach(width; segmentsWidths[segmentIndex][codepointStart .. codepointEnd]) {
1290 						segmentWidth += width;
1291 					}
1292 
1293 					auto diFragment = di;
1294 					diFragment.boundingBox.right = diFragment.boundingBox.left + segmentWidth;
1295 
1296 					// FIXME: adjust the rest of di for this
1297 					// FIXME: the caretInformation arguably should be truncated for those not in this particular sub-segment
1298 					exit = !dg(txt[start .. end], style, diFragment, caretInformation[0 .. cic]);
1299 
1300 					di.initialBaseline.x += segmentWidth;
1301 					di.boundingBox.left += segmentWidth;
1302 
1303 					lastSelPos = end;
1304 					lastSelCodepoint = codepointEnd;
1305 				}
1306 
1307 				size_t codepoint = 0;
1308 
1309 				foreach(ci, dchar ch; txt) {
1310 					auto sel = selectionsAt(cast(int) ci + segment.textBeginOffset);
1311 					if(sel != lastSel) {
1312 						// send this segment
1313 
1314 						sendSegment(lastSelPos, ci, lastSelCodepoint, codepoint);
1315 						lastSel = sel;
1316 						if(exit) return false;
1317 					}
1318 
1319 					codepoint++;
1320 				}
1321 
1322 				sendSegment(lastSelPos, txt.length, lastSelCodepoint, codepoint);
1323 				if(exit) return false;
1324 			}
1325 
1326 			return true;
1327 		}, box);
1328 	}
1329 
1330 	// returns any segments that may lie inside the bounding box. if the box's size is 0, it is unbounded and goes through all segments
1331 	// may return more than is necessary; it uses the box as a hint to speed the search, not as the strict bounds it returns.
1332 	void getInternalSegments(scope bool delegate(size_t idx, scope ref Segment segment) dg, Rectangle box = Rectangle.init) {
1333 		relayoutIfNecessary();
1334 
1335 		if(box.right == box.left)
1336 			box.right = int.max;
1337 		if(box.bottom == box.top)
1338 			box.bottom = int.max;
1339 
1340 		if(segments.length < 64 || box.top < 64) {
1341 			foreach(idx, ref segment; segments) {
1342 				if(dg(idx, segment) == false)
1343 					break;
1344 			}
1345 		} else {
1346 			int maximum = cast(int) segments.length;
1347 			int searchPoint = maximum / 2;
1348 
1349 			keepSearching:
1350 			//writeln(searchPoint);
1351 			if(segments[searchPoint].upperLeft.y > box.top) {
1352 				// we're too far ahead to find the box
1353 				maximum = searchPoint;
1354 				auto newSearchPoint = maximum / 2;
1355 				if(newSearchPoint == searchPoint) {
1356 					searchPoint = newSearchPoint;
1357 					goto useIt;
1358 				}
1359 				searchPoint = newSearchPoint;
1360 				goto keepSearching;
1361 			} else if(segments[searchPoint].boundingBox.bottom < box.top) {
1362 				// the box is a way down from here still
1363 				auto newSearchPoint = (maximum - searchPoint) / 2 + searchPoint;
1364 				if(newSearchPoint == searchPoint) {
1365 					searchPoint = newSearchPoint;
1366 					goto useIt;
1367 				}
1368 				searchPoint = newSearchPoint;
1369 				goto keepSearching;
1370 			}
1371 
1372 			useIt:
1373 
1374 			auto line = segments[searchPoint].displayLineNumber;
1375 			if(line) {
1376 				// go to the line right before this to ensure we have everything in here
1377 				while(searchPoint != 0 && segments[searchPoint].displayLineNumber == line)
1378 					searchPoint--;
1379 			}
1380 
1381 			foreach(idx, ref segment; segments[searchPoint .. $]) {
1382 				if(dg(idx + searchPoint, segment) == false)
1383 					break;
1384 			}
1385 		}
1386 	}
1387 
1388 	private {
1389 		// user code can add new styles to the palette
1390 		TextStyle[] stylePalette;
1391 
1392 		// if editable by user, these will change
1393 		char[] text;
1394 		StyleBlock[] styles;
1395 
1396 		// the layout function calculates these
1397 		Segment[] segments;
1398 		short[][] segmentsWidths;
1399 	}
1400 
1401 	/++
1402 
1403 	+/
1404 	this(TextStyle defaultStyle) {
1405 		this.stylePalette ~= defaultStyle;
1406 		this.text = [0]; // i never want to let the editor go over, so this pseudochar makes that a bit easier
1407 		this.styles ~= StyleBlock(0, 1, 0); // default style should never be deleted too at the end of the file
1408 		this.invalidateLayout(0, 1, 0);
1409 	}
1410 
1411 	// maybe unstable
1412 	TextStyle defaultStyle() {
1413 		auto ts = this.stylePalette[0];
1414 		invalidateLayout(0, text.length, 0); // assume they are going to mutate it
1415 		return ts;
1416 	}
1417 
1418 	bool editable;
1419 	int wordWrapLength = 0;
1420 	int delegate(int x) tabStop = null;
1421 	int delegate(Rectangle line) leftOffset = null;
1422 	int delegate(Rectangle line) rightOffset = null;
1423 	int lineSpacing = 0;
1424 
1425 	/+
1426 		the function it could call is drawStringSegment with a certain slice of it, an offset (guaranteed to be rectangular) and then you do the styles. it does need to know the font tho.
1427 
1428 		it takes a flag: UpperLeft or Baseline. this tells its coordinates for the string segment when you draw.
1429 
1430 		The style can just be a void* or something, not really the problem of the layouter; it only cares about font metrics
1431 
1432 		The layout thing needs to know:
1433 			1) is it word wrapped
1434 			2) a delegate for offset left for the given line height
1435 			2) a delegate for offset right for the given line height
1436 
1437 		GetSelection() returns the segments that are currently selected
1438 		Caret position, if there is one
1439 
1440 		Each TextLayouter can represent a block element in HTML terms. Centering and such done outside.
1441 		Selections going across blocks is an exercise for the outside code (it'd select start to all of one, all of middle, all to end of last).
1442 
1443 
1444 		EDITING:
1445 			just like addText which it does replacing the selection if there is one or inserting/overstriking at the caret
1446 
1447 			everything has an optional void* style which it does as offset-based overlays
1448 
1449 			user responsibility to getSelection if they want to add something to the style
1450 	+/
1451 
1452 	private static struct Segment {
1453 		// 32 bytes rn, i can reasonably save 6 with shorts
1454 		// do i even need the segmentsWidths cache or can i reasonably recalculate it lazily?
1455 
1456 		int textBeginOffset;
1457 		int textEndOffset; // can make a short length i think
1458 
1459 		int styleInformationIndex;
1460 
1461 		// calculated values after iterating through the segment
1462 		int width; // short
1463 		int height; // short
1464 
1465 		Point upperLeft;
1466 
1467 		int displayLineNumber; // I might change this to be a fractional thing, like 24 bits line number, 8 bits fractional number (from word wrap) tho realistically i suspect an index of original lines would be easier to maintain (could only have one value per like 100 real lines cuz it just narrows down the linear search
1468 
1469 		/*
1470 		Point baseline() {
1471 
1472 		}
1473 		*/
1474 
1475 		Rectangle boundingBox() {
1476 			return Rectangle(upperLeft, Size(width, height));
1477 		}
1478 	}
1479 
1480 	private int _width;
1481 	private int _height;
1482 
1483 	private SelectionImpl[64] _selections;
1484 	private int selectionsInUse = 1;
1485 
1486 	/++
1487 		Selections have two parts: an anchor (typically set to where the user clicked the mouse down)
1488 		and a focus (typically where the user released the mouse button). As you click and drag, you
1489 		want to change the focus while keeping the anchor the same.
1490 
1491 		The caret is drawn at the focus. If the anchor and focus are the same point, the selection
1492 		is empty.
1493 
1494 		Please note that the selection focus is different than a keyboard focus. (I'd personally prefer
1495 		to call it a terminus, but I'm trying to use the same terminology as the web standards, even if
1496 		I don't like it.)
1497 
1498 		After calling this, you don't need to call relayout(), but you might want to redraw to show the
1499 		user the result of this action.
1500 	+/
1501 
1502 	// FIXME: the public one might be like segmentOfClick so you can get the style info out (which might hold hyperlink data)
1503 
1504 	/+
1505 		Returns the nearest offset in the text for the given point.
1506 
1507 		it should return if it was inside the segment bounding box tho
1508 	+/
1509 	int offsetOfClick(Point p) {
1510 		int idx = cast(int) text.length - 1;
1511 
1512 		relayoutIfNecessary();
1513 
1514 		if(p.y > _height)
1515 			return idx;
1516 
1517 		getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
1518 			idx = segment.textBeginOffset;
1519 			// FIXME: this all assumes ltr
1520 
1521 			auto boundingBox = Rectangle(segment.upperLeft, Size(segment.width, segment.height));
1522 			if(boundingBox.contains(p)) {
1523 				int x = segment.upperLeft.x;
1524 				int codePointIndex = 0;
1525 
1526 				int bestHit = int.max;
1527 				int bestHitDistance = int.max;
1528 				if(bestHitDistance < 0) bestHitDistance = -bestHitDistance;
1529 				foreach(i, dchar ch; text[segment.textBeginOffset .. segment.textEndOffset]) {
1530 					const width = segmentsWidths[segmentIndex][codePointIndex];
1531 					idx = segment.textBeginOffset + cast(int) i; // can't just idx++ since it needs utf-8 stride
1532 
1533 					auto distanceToLeft = p.x - x;
1534 					if(distanceToLeft < 0) distanceToLeft = -distanceToLeft;
1535 
1536 					//auto distanceToRight = p.x - (x + width);
1537 					//if(distanceToRight < 0) distanceToRight = -distanceToRight;
1538 
1539 					//bool improved = false;
1540 
1541 					if(distanceToLeft < bestHitDistance) {
1542 						bestHit = idx;
1543 						bestHitDistance = distanceToLeft;
1544 						// improved = true;
1545 					}
1546 					/*
1547 					if(distanceToRight < bestHitDistance) {
1548 						bestHit = idx + 1;
1549 						bestHitDistance = distanceToRight;
1550 						improved = true;
1551 					}
1552 					*/
1553 
1554 					//if(!improved) {
1555 						// we're moving further away, no point continuing
1556 						// (please note that RTL transitions = different segment)
1557 						//break;
1558 					//}
1559 
1560 					x += width;
1561 					codePointIndex++;
1562 				}
1563 
1564 				if(bestHit != int.max)
1565 					idx = bestHit;
1566 
1567 				return false;
1568 			} else if(p.x < boundingBox.left && p.y >= boundingBox.top && p.y < boundingBox.bottom) {
1569 				// to the left of a line
1570 				// assumes ltr
1571 				idx = segment.textBeginOffset;
1572 				return false;
1573 			/+
1574 			} else if(p.x >= boundingBox.right && p.y >= boundingBox.top && p.y < boundingBox.bottom) {
1575 				// to the right of a line
1576 				idx = segment.textEndOffset;
1577 				return false;
1578 			+/
1579 			} else if(p.y < segment.upperLeft.y) {
1580 				// should go to the end of the previous line
1581 				auto thisLine = segment.displayLineNumber;
1582 				idx = 0;
1583 				while(segmentIndex > 0) {
1584 					segmentIndex--;
1585 
1586 					if(segments[segmentIndex].displayLineNumber < thisLine) {
1587 						idx = segments[segmentIndex].textEndOffset - 1;
1588 						break;
1589 					}
1590 				}
1591 				return false;
1592 			} else {
1593 				// for single line if nothing else matched we'd best put it at the end; will be reset for the next iteration
1594 				// if there is one. and if not, this is where we want it - at the end of the text
1595 				idx = cast(int) text.length - 1;
1596 			}
1597 
1598 			return true;
1599 		}, Rectangle(p, Size(0, 0)));
1600 		return idx;
1601 	}
1602 
1603 	private StyleHandle getInsertionStyleAt(int offset) {
1604 		assert(offset >= 0 && offset < text.length);
1605 		/+
1606 			If we are at the first part of a logical line, use the next local style (the one in bounds at the offset).
1607 
1608 			Otherwise, use the previous one (the one in bounds).
1609 		+/
1610 
1611 		if(offset == 0 || text[offset - 1] == '\n') {
1612 			// no adjust needed, we use the style here
1613 		} else {
1614 			offset--; // use the previous one
1615 		}
1616 
1617 		return getStyleAt(offset);
1618 	}
1619 
1620 	private StyleHandle getStyleAt(int offset) {
1621 		// FIXME: binary search
1622 		foreach(style; styles) {
1623 			if(offset >= style.offset && offset < (style.offset + style.length))
1624 				return StyleHandle(style.styleInformationIndex);
1625 		}
1626 		assert(0);
1627 	}
1628 
1629 	ulong selectionsAt(int offset) {
1630 		ulong result;
1631 		ulong bit = 1;
1632 		foreach(selection; _selections[0 .. selectionsInUse]) {
1633 			if(selection.containsOffset(offset))
1634 				result |= bit;
1635 			bit <<= 1;
1636 		}
1637 		return result;
1638 	}
1639 
1640 	private int wordWrapWidth_;
1641 
1642 	/++
1643 		Set to 0 to disable word wrapping.
1644 	+/
1645 	public void wordWrapWidth(int width) {
1646 		if(width != wordWrapWidth_) {
1647 			wordWrapWidth_ = width;
1648 			invalidateLayout(0, text.length, 0);
1649 		}
1650 	}
1651 
1652 	private int justificationWidth_;
1653 
1654 	/++
1655 
1656 	+/
1657 	public void justificationWidth(int width) {
1658 		if(width != justificationWidth_) {
1659 			justificationWidth_ = width;
1660 			invalidateLayout(0, text.length, 0);
1661 		}
1662 	}
1663 
1664 	protected bool isWordwrapPoint(dchar c) {
1665 		if(c == ' ')
1666 			return true;
1667 		return false;
1668 	}
1669 
1670 	private bool invalidateLayout_;
1671 	private int invalidStart = int.max;
1672 	private int invalidEnd = 0;
1673 	private int invalidatedChangeInTextLength = 0;
1674 	/++
1675 		This should be called (internally, end users don't need to see it) any time the text or style has changed.
1676 	+/
1677 	protected void invalidateLayout(size_t start, size_t end, int changeInTextLength) {
1678 		invalidateLayout_ = true;
1679 
1680 		if(start < invalidStart)
1681 			invalidStart = cast(int) start;
1682 		if(end > invalidEnd)
1683 			invalidEnd = cast(int) end;
1684 
1685 		invalidatedChangeInTextLength += changeInTextLength;
1686 	}
1687 
1688 	/++
1689 		This should be called (internally, end users don't need to see it) any time you're going to return something to the user that is dependent on the layout.
1690 	+/
1691 	protected void relayoutIfNecessary() {
1692 		if(invalidateLayout_) {
1693 			relayoutImplementation();
1694 			invalidateLayout_ = false;
1695 			invalidStart = int.max;
1696 			invalidEnd = 0;
1697 			invalidatedChangeInTextLength = 0;
1698 		}
1699 	}
1700 
1701 	/++
1702 		Params:
1703 			wordWrapLength = the length, in display pixels, of the layout's bounding box as far as word wrap is concerned. If 0, word wrapping is disabled.
1704 
1705 			FIXME: wordWrapChars and if you word wrap, should it indent it too? more realistically i pass the string to the helper and it has to findWordBoundary and then it can prolly return the left offset too, based on the previous line offset perhaps.
1706 
1707 			substituteGlyph?  actually that can prolly be a fake password font.
1708 
1709 
1710 			int maximumHeight. if non-zero, the leftover text is returned so you can pass it to another layout instance (e.g. for columns or pagination)
1711 	+/
1712 	protected void relayoutImplementation() {
1713 
1714 
1715 		// an optimization here is to avoid redoing stuff outside the invalidated zone.
1716 		// basically it would keep going until a segment after the invalidated end area was in the state before and after.
1717 
1718 		debug(text_layouter_bench) {
1719 			// writeln("relayouting");
1720 			import core.time;
1721 			auto start = MonoTime.currTime;
1722 			scope(exit) {
1723 				writeln(MonoTime.currTime - start);
1724 			}
1725 		}
1726 
1727 		auto originalSegments = segments;
1728 		auto originalWidth = _width;
1729 		auto originalHeight = _height;
1730 		auto originalSegmentsWidths = segmentsWidths;
1731 
1732 		_width = 0;
1733 		_height = 0;
1734 
1735 		assert(invalidStart != int.max);
1736 		assert(invalidStart >= 0);
1737 		assert(invalidStart < text.length);
1738 
1739 		if(invalidEnd > text.length)
1740 			invalidEnd = cast(int) text.length;
1741 
1742 		int firstInvalidSegment = 0;
1743 
1744 		Point currentCorner = Point(0, 0);
1745 		int displayLineNumber = 0;
1746 		int lineSegmentIndexStart = 0;
1747 
1748 		if(invalidStart != 0) {
1749 			// while i could binary search for the invalid thing,
1750 			// i also need to rebuild _width and _height anyway so
1751 			// just gonna loop through and hope for the best.
1752 			bool found = false;
1753 
1754 			// I can't just use the segment bounding box per se since that isn't the whole line
1755 			// and the finishLine adjustment for mixed fonts/sizes will throw things off. so we
1756 			// want to start at the very corner of the line
1757 			int lastLineY;
1758 			int thisLineY;
1759 			foreach(idx, segment; segments) {
1760 				// FIXME: i might actually need to go back to the logical line
1761 				if(displayLineNumber != segment.displayLineNumber) {
1762 					lastLineY = thisLineY;
1763 					displayLineNumber = segment.displayLineNumber;
1764 					lineSegmentIndexStart = cast(int) idx;
1765 				}
1766 				auto b = segment.boundingBox.bottom;
1767 				if(b > thisLineY)
1768 					thisLineY = b;
1769 
1770 				if(invalidStart >= segment.textBeginOffset  && invalidStart < segment.textEndOffset) {
1771 					// we'll redo the whole line with the invalidated region since it might have other coordinate things
1772 
1773 					segment = segments[lineSegmentIndexStart];
1774 
1775 					firstInvalidSegment = lineSegmentIndexStart;// cast(int) idx;
1776 					invalidStart = segment.textBeginOffset;
1777 					displayLineNumber = segment.displayLineNumber;
1778 					currentCorner = segment.upperLeft;
1779 					currentCorner.y = lastLineY;
1780 
1781 					found = true;
1782 					break;
1783 				}
1784 
1785 				// FIXME: since we rewind to the line segment start above this might not be correct anymore.
1786 				auto bb = segment.boundingBox;
1787 				if(bb.right > _width)
1788 					_width = bb.right;
1789 				if(bb.bottom > _height)
1790 					_height = bb.bottom;
1791 			}
1792 			assert(found);
1793 		}
1794 
1795 		// writeln(invalidStart, " starts segment ", firstInvalidSegment, " and line ", displayLineNumber, " seg ", lineSegmentIndexStart);
1796 
1797 		segments = segments[0 .. firstInvalidSegment];
1798 		segments.assumeSafeAppend();
1799 
1800 		segmentsWidths = segmentsWidths[0 .. firstInvalidSegment];
1801 		segmentsWidths.assumeSafeAppend();
1802 
1803 		version(try_kerning_hack) {
1804 			size_t previousIndex = 0;
1805 			int lastWidth;
1806 			int lastWidthDistance;
1807 		}
1808 
1809 		Segment segment;
1810 
1811 		Segment previousOldSavedSegment;
1812 		short[] previousOldSavedWidths;
1813 		TextStyle currentStyle = null;
1814 		int currentStyleIndex = 0;
1815 		MeasurableFont font;
1816 		ubyte[128] glyphWidths;
1817 		void loadNewFont(MeasurableFont what) {
1818 			font = what;
1819 
1820 			// caching the ascii widths locally can give a boost to ~ 20% of the speed of this function
1821 			foreach(char c; 32 .. 128) {
1822 				auto w = font.stringWidth((&c)[0 .. 1]);
1823 				glyphWidths[c] = cast(ubyte) w; // FIXME: what if it doesn't fit?
1824 			}
1825 		}
1826 
1827 		auto styles = this.styles;
1828 
1829 		foreach(style; this.styles) {
1830 			if(invalidStart >= style.offset && invalidStart < (style.offset + style.length)) {
1831 				currentStyle = stylePalette[style.styleInformationIndex];
1832 				if(currentStyle !is null)
1833 					loadNewFont(currentStyle.font);
1834 				currentStyleIndex = style.styleInformationIndex;
1835 
1836 				styles = styles[1 .. $];
1837 				break;
1838 			} else if(style.offset > invalidStart) {
1839 				break;
1840 			}
1841 			styles = styles[1 .. $];
1842 		}
1843 
1844 		int offsetToNextStyle = int.max;
1845 		if(styles.length) {
1846 			offsetToNextStyle = styles[0].offset;
1847 		}
1848 
1849 
1850 		assert(offsetToNextStyle >= 0);
1851 
1852 		short[] widths;
1853 
1854 		size_t segmentBegan = invalidStart;
1855 		void finishSegment(size_t idx) {
1856 			if(idx == segmentBegan)
1857 				return;
1858 			segmentBegan = idx;
1859 			segment.textEndOffset = cast(int) idx;
1860 			segment.displayLineNumber = displayLineNumber;
1861 
1862 			if(segments.length < originalSegments.length) {
1863 				previousOldSavedSegment = originalSegments[segments.length];
1864 				previousOldSavedWidths = originalSegmentsWidths[segmentsWidths.length];
1865 			} else {
1866 				previousOldSavedSegment = Segment.init;
1867 				previousOldSavedWidths = null;
1868 			}
1869 
1870 			segments ~= segment;
1871 			segmentsWidths ~= widths;
1872 
1873 			segment = Segment.init;
1874 			segment.upperLeft = currentCorner;
1875 			segment.styleInformationIndex = currentStyleIndex;
1876 			segment.textBeginOffset = cast(int) idx;
1877 			widths = null;
1878 		}
1879 
1880 		// FIXME: when we start in an invalidated thing this is not necessarily right, it should be calculated above
1881 		int biggestDescent = font.descent;
1882 		int lineHeight = font.height;
1883 
1884 		bool finishLine(size_t idx, MeasurableFont outerFont) {
1885 			if(segment.textBeginOffset == idx)
1886 				return false; // no need to keep nothing.
1887 
1888 			if(currentCorner.x > this._width)
1889 				this._width = currentCorner.x;
1890 
1891 			auto thisLineY = currentCorner.y;
1892 
1893 			auto thisLineHeight = lineHeight;
1894 			currentCorner.y += lineHeight;
1895 			currentCorner.x = 0;
1896 
1897 			finishSegment(idx); // i use currentCorner in there! so this must be after that
1898 			displayLineNumber++;
1899 
1900 			lineHeight = outerFont.height;
1901 			biggestDescent = outerFont.descent;
1902 
1903 			// go back and adjust all the segments on this line to have the right height and do vertical alignment with the baseline
1904 			foreach(ref seg; segments[lineSegmentIndexStart .. $]) {
1905 				MeasurableFont font;
1906 				if(seg.styleInformationIndex < stylePalette.length) {
1907 					auto si = stylePalette[seg.styleInformationIndex];
1908 					if(si)
1909 						font = si.font;
1910 				}
1911 
1912 				auto baseline = thisLineHeight - biggestDescent;
1913 
1914 				seg.upperLeft.y += baseline - font.ascent;
1915 				seg.height = thisLineHeight - (baseline - font.ascent);
1916 			}
1917 
1918 			// now need to check if we can finish relayout early
1919 
1920 			// if we're beyond the invalidated section and have original data to compare against...
1921 			previousOldSavedSegment.textBeginOffset += invalidatedChangeInTextLength;
1922 			previousOldSavedSegment.textEndOffset += invalidatedChangeInTextLength;
1923 
1924 			/+
1925 			// FIXME: would be nice to make this work somehow - when you input a new line it needs to just adjust the y stuff
1926 			// part of the problem is that it needs to inject a new segment for the newline and then the whole old array is
1927 			// broken.
1928 			int deltaY;
1929 			int deltaLineNumber;
1930 
1931 			if(idx >= invalidEnd && segments[$-1] != previousOldSavedSegment) {
1932 				deltaY = thisLineHeight;
1933 				deltaLineNumber = 1;
1934 				previousOldSavedSegment.upperLeft.y += deltaY;
1935 				previousOldSavedSegment.displayLineNumber += deltaLineNumber;
1936 				writeln("trying deltaY = ", deltaY);
1937 				writeln(previousOldSavedSegment);
1938 				writeln(segments[$-1]);
1939 			}
1940 			+/
1941 
1942 			// FIXME: if the only thing that's changed is a y coordinate, adjust that too
1943 			// finishEarly();
1944 			if(idx >= invalidEnd && segments[$-1] == previousOldSavedSegment) {
1945 				if(segmentsWidths[$-1] == previousOldSavedWidths) {
1946 					// we've hit a point where nothing has changed, it is time to stop processing
1947 
1948 					foreach(ref seg; originalSegments[segments.length .. $]) {
1949 						seg.textBeginOffset += invalidatedChangeInTextLength;
1950 						seg.textEndOffset += invalidatedChangeInTextLength;
1951 
1952 						/+
1953 						seg.upperLeft.y += deltaY;
1954 						seg.displayLineNumber += deltaLineNumber;
1955 						+/
1956 
1957 						auto bb = seg.boundingBox;
1958 						if(bb.right > _width)
1959 							_width = bb.right;
1960 						if(bb.bottom > _height)
1961 							_height = bb.bottom;
1962 					}
1963 
1964 					// these refer to the same array or should anyway so hopefully this doesn't do anything.
1965 					// FIXME: confirm this isn't sucky
1966 					segments ~= originalSegments[segments.length .. $];
1967 					segmentsWidths ~= originalSegmentsWidths[segmentsWidths.length .. $];
1968 
1969 					return true;
1970 				} else {
1971 					// writeln("not matched");
1972 					// writeln(previousOldSavedWidths != segmentsWidths[$-1]);
1973 				}
1974 			}
1975 
1976 			lineSegmentIndexStart = cast(int) segments.length;
1977 
1978 			return false;
1979 		}
1980 
1981 		void finishEarly() {
1982 			// lol i did all the work before triggering this
1983 		}
1984 
1985 		segment.upperLeft = currentCorner;
1986 		segment.styleInformationIndex = currentStyleIndex;
1987 		segment.textBeginOffset = invalidStart;
1988 
1989 		bool endSegment;
1990 		bool endLine;
1991 
1992 		bool tryWordWrapOnNext;
1993 
1994 		// writeln("Prior to loop: ", MonoTime.currTime - start, " ", invalidStart);
1995 
1996 		// FIXME: i should prolly go by grapheme
1997 		foreach(idxRaw, dchar ch; text[invalidStart .. $]) {
1998 			auto idx = idxRaw + invalidStart;
1999 
2000 			version(try_kerning_hack)
2001 				lastWidthDistance++;
2002 			auto oldFont = font;
2003 			if(offsetToNextStyle == idx) {
2004 				auto oldStyle = currentStyle;
2005 				if(styles.length) {
2006 					StyleBlock currentStyleBlock = styles[0];
2007 					offsetToNextStyle += currentStyleBlock.length;
2008 					styles = styles[1 .. $];
2009 
2010 					currentStyle = stylePalette[currentStyleBlock.styleInformationIndex];
2011 					currentStyleIndex = currentStyleBlock.styleInformationIndex;
2012 				} else {
2013 					currentStyle = null;
2014 					offsetToNextStyle = int.max;
2015 				}
2016 				if(oldStyle !is currentStyle) {
2017 					if(!endLine)
2018 						endSegment = true;
2019 
2020 					loadNewFont(currentStyle.font);
2021 				}
2022 			}
2023 
2024 			if(tryWordWrapOnNext) {
2025 				int nextWordwrapPoint = cast(int) idx;
2026 				while(nextWordwrapPoint < text.length && !isWordwrapPoint(text[nextWordwrapPoint])) {
2027 					if(text[nextWordwrapPoint] == '\n')
2028 						break;
2029 					nextWordwrapPoint++;
2030 				}
2031 
2032 				if(currentCorner.x + font.stringWidth(text[idx .. nextWordwrapPoint]) >= wordWrapWidth_)
2033 					endLine = true;
2034 
2035 				tryWordWrapOnNext = false;
2036 			}
2037 
2038 			if(endSegment && !endLine) {
2039 				finishSegment(idx);
2040 				endSegment = false;
2041 			}
2042 
2043 			bool justChangedLine;
2044 			if(endLine) {
2045 				auto flr = finishLine(idx, oldFont);
2046 				if(flr)
2047 					return finishEarly();
2048 				endLine = false;
2049 				endSegment = false;
2050 				justChangedLine = true;
2051 			}
2052 
2053 			if(font !is oldFont) {
2054 				// FIXME: adjust height
2055 				if(justChangedLine || font.height > lineHeight)
2056 					lineHeight = font.height;
2057 				if(justChangedLine || font.descent > biggestDescent)
2058 					biggestDescent = font.descent;
2059 			}
2060 
2061 
2062 
2063 			int thisWidth = 0;
2064 
2065 			switch(ch) {
2066 				case 0:
2067 					goto advance;
2068 				case '\r':
2069 					goto advance;
2070 				case '\n':
2071 					/+
2072 					finishSegment(idx);
2073 					segment.textBeginOffset = cast(int) idx;
2074 
2075 					thisWidth = 0;
2076 					+/
2077 
2078 					endLine = true;
2079 					goto advance;
2080 
2081 					// FIXME: a tab at the end of a line causes the next line to indent
2082 				case '\t':
2083 					finishSegment(idx);
2084 
2085 					// a tab should be its own segment with no text
2086 					// per se
2087 
2088 					thisWidth = 48;
2089 
2090 					segment.width += thisWidth;
2091 					currentCorner.x += thisWidth;
2092 
2093 					endSegment = true;
2094 					goto advance;
2095 
2096 					//goto advance;
2097 				default:
2098 					// FIXME: i don't think the draw thing uses kerning but if it does this is wrong.
2099 
2100 					// figure out this length (it uses previous idx to get some kerning info used)
2101 					version(try_kerning_hack) {
2102 						if(lastWidthDistance == 1) {
2103 							auto width = font.stringWidth(text[previousIndex .. idx + stride(text[idx])]);
2104 							thisWidth = width - lastWidth;
2105 							// writeln(text[previousIndex .. idx + stride(text[idx])], " ", width, "-", lastWidth);
2106 						} else {
2107 							auto width = font.stringWidth(text[idx .. idx + stride(text[idx])]);
2108 							thisWidth = width;
2109 						}
2110 					} else {
2111 						if(text[idx] < 128)
2112 							thisWidth = glyphWidths[text[idx]];
2113 						else
2114 							thisWidth = font.stringWidth(text[idx .. idx + stride(text[idx])]);
2115 					}
2116 
2117 					segment.width += thisWidth;
2118 					currentCorner.x += thisWidth;
2119 
2120 					version(try_kerning_hack) {
2121 						lastWidth = thisWidth;
2122 						previousIndex = idx;
2123 						lastWidthDistance = 0;
2124 					}
2125 			}
2126 
2127 			if(wordWrapWidth_ > 0 && isWordwrapPoint(ch))
2128 				tryWordWrapOnNext = true;
2129 
2130 			// if im iterating and hit something that would change the line height, will have to go back and change everything perhaps. or at least work with offsets from the baseline throughout...
2131 
2132 			// might also just want a special string sequence that can inject things in the middle of text like inline images. it'd have to tell the height and advance.
2133 
2134 			// this would be to test if the kerning adjustments do anything. seems like the fonts
2135 			// don't care tbh but still.
2136 			// thisWidth = font.stringWidth(text[idx .. idx + stride(text[idx])]);
2137 
2138 			advance:
2139 			if(segment.textBeginOffset != -1) {
2140 				widths ~= cast(short) thisWidth;
2141 			}
2142 		}
2143 
2144 		finishLine(text.length, font);
2145 
2146 		_height = currentCorner.y;
2147 
2148 		assert(segments.length);
2149 
2150 		//return widths;
2151 
2152 		// writefln("%(%s\n%)", segments[0 .. 10]);
2153 	}
2154 
2155 	private {
2156 		int stride(char c) {
2157 			if(c < 0x80) {
2158 				return 1;
2159 			} else if(c == 0xff) {
2160 				return 1;
2161 			} else {
2162 				import core.bitop : bsr;
2163 				return 7 - bsr((~uint(c)) & 0xFF);
2164 			}
2165 		}
2166 	}
2167 }
2168 
2169 class StyledTextLayouter(StyleClass) : TextLayouter {
2170 
2171 }