The OpenD Programming Language

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 		// layouter.notifySelectionChanged();
319 		return this;
320 	}
321 
322 	/// ditto
323 	Selection setFocus() {
324 		impl.focus = impl.position;
325 		// layouter.notifySelectionChanged();
326 		return this;
327 	}
328 
329 	/// ditto
330 	Selection setUserCoordinate(Point p) {
331 		impl.virtualFocusPosition = p;
332 		return this;
333 	}
334 
335 	/// ditto
336 	Selection setUserXCoordinate() {
337 		impl.virtualFocusPosition.x = layouter.boundingBoxOfGlyph(layouter.findContainingSegment(impl.position), impl.position).left;
338 		return this;
339 	}
340 
341 	/// ditto
342 	Selection setUserYCoordinate() {
343 		impl.virtualFocusPosition.y = layouter.boundingBoxOfGlyph(layouter.findContainingSegment(impl.position), impl.position).top;
344 		return this;
345 	}
346 
347 
348 	/+ Moving the internal position +/
349 
350 	/++
351 
352 	+/
353 	Selection moveTo(Point p, bool setUserCoordinate = true) {
354 		impl.position = layouter.offsetOfClick(p);
355 		if(setUserCoordinate)
356 			impl.virtualFocusPosition = p;
357 		return this;
358 	}
359 
360 	/++
361 
362 	+/
363 	Selection moveToStartOfDocument() {
364 		impl.position = 0;
365 		return this;
366 	}
367 
368 	/// ditto
369 	Selection moveToEndOfDocument() {
370 		impl.position = cast(int) layouter.text.length - 1; // never include the 0 terminator
371 		return this;
372 	}
373 
374 	/++
375 
376 	+/
377 	Selection moveToStartOfLine(bool byRender = true, bool includeLeadingWhitespace = true) {
378 		// FIXME: chekc for word wrap by checking segment.displayLineNumber
379 		// FIXME: includeLeadingWhitespace
380 		while(impl.position > 0 && layouter.text[impl.position - 1] != '\n')
381 			impl.position--;
382 
383 		return this;
384 	}
385 
386 	/// ditto
387 	Selection moveToEndOfLine(bool byRender = true) {
388 		// FIXME: chekc for word wrap by checking segment.displayLineNumber
389 		while(impl.position + 1 < layouter.text.length && layouter.text[impl.position] != '\n') // never include the 0 terminator
390 			impl.position++;
391 		return this;
392 	}
393 
394 	/++
395 		If the position is abutting an end of line marker, it moves past it, to include it.
396 		If not, it does nothing.
397 
398 		The intention is so you can delete a whole line by doing:
399 
400 		---
401 		with(selection) {
402 			moveToStartOfLine();
403 			setAnchor();
404 			// this moves to the end of the visible line, but if you stopped here, you'd be left with an empty line
405 			moveToEndOfLine();
406 			// this moves past the line marker, meaning you don't just delete the line's content, it deletes the entire line
407 			moveToIncludeAdjacentEndOfLineMarker();
408 			setFocus();
409 			replaceContent("");
410 		}
411 		---
412 	+/
413 	Selection moveToIncludeAdjacentEndOfLineMarker() {
414 		// FIXME: i need to decide what i want to do about \r too. Prolly should remove it at the boundaries.
415 		if(impl.position + 1 < layouter.text.length && layouter.text[impl.position] == '\n') { // never include the 0 terminator
416 			impl.position++;
417 		}
418 		return this;
419 	}
420 
421 	// note there's move up / down / left / right
422 	// in addition to move forward / backward glyph/line
423 	// the directions always match what's on screen.
424 	// the others always match the logical order in the string.
425 	/++
426 
427 	+/
428 	Selection moveUp(int count = 1, bool byRender = true) {
429 		verticalMoveImpl(-1, count, byRender);
430 		return this;
431 	}
432 
433 	/// ditto
434 	Selection moveDown(int count = 1, bool byRender = true) {
435 		verticalMoveImpl(1, count, byRender);
436 		return this;
437 	}
438 
439 	/// ditto
440 	Selection moveLeft(int count = 1, bool byRender = true) {
441 		horizontalMoveImpl(-1, count, byRender);
442 		return this;
443 	}
444 
445 	/// ditto
446 	Selection moveRight(int count = 1, bool byRender = true) {
447 		horizontalMoveImpl(1, count, byRender);
448 		return this;
449 	}
450 
451 	/+
452 	enum PlacementOfFind {
453 		beginningOfHit,
454 		endOfHit
455 	}
456 
457 	enum IfNotFound {
458 		changeNothing,
459 		moveToEnd,
460 		callDelegate
461 	}
462 
463 	enum CaseSensitive {
464 		yes,
465 		no
466 	}
467 
468 	void find(scope const(char)[] text, PlacementOfFind placeAt = PlacementOfFind.beginningOfHit, IfNotFound ifNotFound = IfNotFound.changeNothing) {
469 	}
470 	+/
471 
472 	/++
473 		Does a custom search through the text.
474 
475 		Params:
476 			predicate = a search filter. It passes you back a slice of your buffer filled with text at the current search position. You pass the slice of this buffer that matched your search, or `null` if there was no match here. You MUST return either null or a slice of the buffer that was passed to you. If you return an empty slice of of the buffer (buffer[0..0] for example), it cancels the search.
477 
478 			The window buffer will try to move one code unit at a time. It may straddle code point boundaries - you need to account for this in your predicate.
479 
480 			windowBuffer = a buffer to temporarily hold text for comparison. You should size this for the text you're trying to find
481 
482 			searchBackward = determines the direction of the search. If true, it searches from the start of current selection backward to the beginning of the document. If false, it searches from the end of current selection forward to the end of the document.
483 		Returns:
484 			an object representing the search results and letting you manipulate the selection based upon it
485 
486 	+/
487 	FindResult find(
488 		scope const(char)[] delegate(scope return const(char)[] buffer) predicate,
489 		int windowBufferSize,
490 		bool searchBackward,
491 	) {
492 		assert(windowBufferSize != 0, "you must pass a buffer of some size");
493 
494 		char[] windowBuffer = new char[](windowBufferSize); // FIXME i don't need to actually copy in the current impl
495 
496 		int currentSpot = impl.position;
497 
498 		const finalSpot = searchBackward ? currentSpot : cast(int) layouter.text.length;
499 
500 		if(searchBackward) {
501 			currentSpot -= windowBuffer.length;
502 			if(currentSpot < 0)
503 				currentSpot = 0;
504 		}
505 
506 		auto endingSpot = currentSpot + windowBuffer.length;
507 		if(endingSpot > finalSpot)
508 			endingSpot = finalSpot;
509 
510 		keep_searching:
511 		windowBuffer[0 .. endingSpot - currentSpot] = layouter.text[currentSpot .. endingSpot];
512 		auto result = predicate(windowBuffer[0 .. endingSpot - currentSpot]);
513 		if(result !is null) {
514 			// we're done, it was found
515 			auto offsetStart = result is null ? currentSpot : cast(int) (result.ptr - windowBuffer.ptr);
516 			assert(offsetStart >= 0 && offsetStart < windowBuffer.length);
517 			return FindResult(this, currentSpot + offsetStart, result !is null, currentSpot + cast(int) (offsetStart + result.length));
518 		} else if((searchBackward && currentSpot > 0) || (!searchBackward && endingSpot < finalSpot)) {
519 			// not found, keep searching
520 			if(searchBackward) {
521 				currentSpot--;
522 				endingSpot--;
523 			} else {
524 				currentSpot++;
525 				endingSpot++;
526 			}
527 			goto keep_searching;
528 		} else {
529 			// not found, at end of search
530 			return FindResult(this, currentSpot, false, currentSpot /* zero length result */);
531 		}
532 
533 		assert(0);
534 	}
535 
536 	/// ditto
537 	static struct FindResult {
538 		private Selection selection;
539 		private int position;
540 		private bool found;
541 		private int endPosition;
542 
543 		///
544 		bool wasFound() {
545 			return found;
546 		}
547 
548 		///
549 		Selection moveTo() {
550 			selection.impl.position = position;
551 			return selection;
552 		}
553 
554 		///
555 		Selection moveToEnd() {
556 			selection.impl.position = endPosition;
557 			return selection;
558 		}
559 
560 		///
561 		void selectHit() {
562 			selection.impl.position = position;
563 			selection.setAnchor();
564 			selection.impl.position = endPosition;
565 			selection.setFocus();
566 		}
567 	}
568 
569 
570 
571 	/+
572 	/+ +
573 		Searches by regex.
574 
575 		This is a template because the regex engine can be a heavy dependency, so it is only
576 		included if you need it. The RegEx object is expected to match the Phobos std.regex.RegEx
577 		api, so while you could, in theory, replace it, it is probably easier to just use the Phobos one.
578 	+/
579 	void find(RegEx)(RegEx re) {
580 
581 	}
582 	+/
583 
584 	/+ Manipulating the data in the selection +/
585 
586 	/++
587 		Replaces the content of the selection. If you replace it with an empty `newText`, it will delete the content.
588 
589 		If newText == "\b", it will delete the selection if it is non-empty, and otherwise delete the thing before the cursor.
590 
591 		If you want to do normal editor backspace key, you might want to check `if(!selection.isEmpty()) selection.moveLeft();`
592 		before calling `selection.deleteContent()`. Similarly, for the delete key, you might use `moveRight` instead, since this
593 		function will do nothing for an empty selection by itself.
594 
595 		FIXME: what if i want to replace it with some multiply styled text? Could probably call it in sequence actually.
596 	+/
597 	Selection replaceContent(scope const(char)[] newText, TextLayouter.StyleHandle style = TextLayouter.StyleHandle.init) {
598 		layouter.wasMutated_ = true;
599 
600 		if(style == TextLayouter.StyleHandle.init)
601 			style = layouter.getInsertionStyleAt(impl.focus);
602 
603 		int removeBegin, removeEnd;
604 		if(this.isEmpty()) {
605 			if(newText.length == 1 && newText[0] == '\b') {
606 				auto place = impl.focus;
607 				if(place > 0) {
608 					int amount = 1;
609 					while((layouter.text[place - amount] & 0b11000000) == 0b10000000) // all non-start bytes of a utf-8 sequence have this convenient property
610 						amount++; // assumes this will never go over the edge cuz of it being valid utf 8 internally
611 
612 					removeBegin = place - amount;
613 					removeEnd = place;
614 
615 					if(removeBegin < 0)
616 						removeBegin = 0;
617 					if(removeEnd < 0)
618 						removeEnd = 0;
619 				}
620 
621 				newText = null;
622 			} else {
623 				removeBegin = impl.terminus;
624 				removeEnd = impl.terminus;
625 			}
626 		} else {
627 			removeBegin = impl.start;
628 			removeEnd = impl.end;
629 			if(newText.length == 1 && newText[0] == '\b') {
630 				newText = null;
631 			}
632 		}
633 
634 		auto place = impl.terminus;
635 
636 		auto changeInLength = cast(int) newText.length - (removeEnd - removeBegin);
637 
638 		// FIXME: the horror
639 		auto trash = layouter.text[0 .. removeBegin];
640 		trash ~= newText;
641 		trash ~= layouter.text[removeEnd .. $];
642 		layouter.text = trash;
643 
644 		impl.position = removeBegin + cast(int) newText.length;
645 		this.setAnchor();
646 
647 		/+
648 			For styles:
649 				if one part resides in the deleted zone, it should be truncated to the edge of the deleted zone
650 				if they are entirely in the deleted zone - their new length is zero - they should simply be deleted
651 				if they are entirely before the deleted zone, it can stay the same
652 				if they are entirely after the deleted zone, they should get += changeInLength
653 
654 				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
655 
656 				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.
657 		+/
658 
659 		//writeln(removeBegin, " ", removeEnd);
660 		//foreach(st; layouter.styles) writeln("B: ", st.offset, "..", st.offset + st.length, " ", st.styleInformationIndex);
661 
662 		// first I'm going to update all of them so it is in a consistent state
663 		foreach(ref st; layouter.styles) {
664 			auto begin = st.offset;
665 			auto end = st.offset + st.length;
666 
667 			void adjust(ref int what) {
668 				if(what < removeBegin) {
669 					// no change needed
670 				} else if(what >= removeBegin && what < removeEnd) {
671 					what = removeBegin;
672 				} else if(what) {
673 					what += changeInLength;
674 				}
675 			}
676 
677 			adjust(begin);
678 			adjust(end);
679 
680 			assert(end >= begin); // empty styles are not permitted by the implementation
681 			st.offset = begin;
682 			st.length = end - begin;
683 		}
684 
685 		// then go back and inject the new style, if needed
686 		if(changeInLength > 0) {
687 			changeStyle(removeBegin, removeBegin + cast(int) newText.length, style);
688 		}
689 
690 		removeEmptyStyles();
691 
692 		// or do i want to use init to just keep using the same style as is already there?
693 		// FIXME
694 		//if(style !is StyleHandle.init) {
695 			// styles ~= StyleBlock(cast(int) before.length, cast(int) changeInLength, style.index);
696 		//}
697 
698 
699 		auto endInvalidate = removeBegin + newText.length;
700 		if(removeEnd > endInvalidate)
701 			endInvalidate = removeEnd;
702 		layouter.invalidateLayout(removeBegin, endInvalidate, changeInLength);
703 
704 		// there's a new style from removeBegin to removeBegin + newText.length
705 
706 		// FIXME other selections in the zone need to be adjusted too
707 		// if they are in the deleted zone, it should be moved to the end of the new zone (removeBegin + newText.length)
708 		// if they are before the deleted zone, they can stay the same
709 		// if they are after the deleted zone, they should be adjusted by changeInLength
710 		foreach(idx, ref selection; layouter._selections[0 .. layouter.selectionsInUse]) {
711 
712 			// don't adjust ourselves here, we already did it above
713 			// and besides don't want mutation in here
714 			if(idx == selectionId)
715 				continue;
716 
717 			void adjust(ref int what) {
718 				if(what < removeBegin) {
719 					// no change needed
720 				} else if(what >= removeBegin && what < removeEnd) {
721 					what = removeBegin;
722 				} else if(what) {
723 					what += changeInLength;
724 				}
725 			}
726 
727 			adjust(selection.anchor);
728 			adjust(selection.terminus);
729 		}
730 			// you might need to set the user coordinate after this!
731 
732 		return this;
733 	}
734 
735 	private void removeEmptyStyles() {
736 		/+ the code doesn't like empty style blocks, so gonna go back and remove those +/
737 		for(int i = 0; i < cast(int) layouter.styles.length; i++) {
738 			if(layouter.styles[i].length == 0) {
739 				for(auto i2 = i; i2 + 1 < layouter.styles.length; i2++)
740 					layouter.styles[i2] = layouter.styles[i2 + 1];
741 				layouter.styles = layouter.styles[0 .. $-1];
742 				layouter.styles.assumeSafeAppend();
743 				i--;
744 			}
745 		}
746 	}
747 
748 	/++
749 		Changes the style of the given selection. Gives existing styles in the selection to your delegate
750 		and you return a new style to assign to that block.
751 	+/
752 	public void changeStyle(TextLayouter.StyleHandle delegate(TextStyle existing) newStyle) {
753 		// FIXME there might be different sub-styles so we should actually look them up and send each one
754 		auto ns = newStyle(null);
755 		changeStyle(impl.start, impl.end, ns);
756 		removeEmptyStyles();
757 
758 		layouter.invalidateLayout(impl.start, impl.end, 0);
759 	}
760 
761 	/+ Impl helpers +/
762 
763 	private void changeStyle(int newStyleBegin, int newStyleEnd, TextLayouter.StyleHandle style) {
764 		// FIXME: binary search
765 		for(size_t i = 0; i < layouter.styles.length; i++) {
766 			auto s = &layouter.styles[i];
767 			const oldStyleBegin = s.offset;
768 			const oldStyleEnd = s.offset + s.length;
769 
770 			if(newStyleBegin >= oldStyleBegin && newStyleBegin < oldStyleEnd) {
771 				// the cases:
772 
773 				// it is an exact match in size, we can simply overwrite it
774 				if(newStyleBegin == oldStyleBegin && newStyleEnd == oldStyleEnd) {
775 					s.styleInformationIndex = style.index;
776 					break; // all done
777 				}
778 				// we're the same as the existing style, so it is just a matter of extending it to include us
779 				else if(s.styleInformationIndex == style.index) {
780 					if(newStyleEnd > oldStyleEnd) {
781 						s.length = newStyleEnd - oldStyleBegin;
782 
783 						// then need to fix up all the subsequent blocks, adding the offset, reducing the length
784 						int remainingFixes = newStyleEnd - oldStyleEnd;
785 						foreach(st; layouter.styles[i + 1 .. $]) {
786 							auto thisFixup = remainingFixes;
787 							if(st.length < thisFixup)
788 								thisFixup = st.length;
789 							// this can result in 0 length, the loop after this will delete that.
790 							st.offset += thisFixup;
791 							st.length -= thisFixup;
792 
793 							remainingFixes -= thisFixup;
794 
795 							assert(remainingFixes >= 0);
796 
797 							if(remainingFixes == 0)
798 								break;
799 						}
800 					}
801 					// otherwise it is all already there and nothing need be done at all
802 					break;
803 				}
804 				// for the rest of the cases, the style does not match and is not a size match,
805 				// so a new block is going to have to be inserted
806 				// ///////////
807 				// we're entirely contained inside, so keep the left, insert ourselves, and re-create right.
808 				else if(newStyleEnd > oldStyleBegin && newStyleEnd < oldStyleEnd) {
809 					// keep the old style on the left...
810 					s.length = newStyleBegin - oldStyleBegin;
811 
812 					auto toInsert1 = TextLayouter.StyleBlock(newStyleBegin, newStyleEnd - newStyleBegin, style.index);
813 					auto toInsert2 = TextLayouter.StyleBlock(newStyleEnd, oldStyleEnd - newStyleEnd, s.styleInformationIndex);
814 
815 					layouter.styles = layouter.styles[0 .. i + 1] ~ toInsert1 ~ toInsert2 ~ layouter.styles[i + 1 .. $];
816 
817 					// writeln(*s); writeln(toInsert1); writeln(toInsert2);
818 
819 					break; // no need to continue processing as the other offsets are unaffected
820 				}
821 				// we need to keep the left end of the original thing, but then insert ourselves on afterward
822 				else if(newStyleBegin >= oldStyleBegin) {
823 					// want to just shorten the original thing, then adjust the values
824 					// so next time through the loop can work on that existing block
825 
826 					s.length = newStyleBegin - oldStyleBegin;
827 
828 					// extend the following style to start here, so there's no gap in the next loop iteration
829 					if(i + i < layouter.styles.length) {
830 						auto originalOffset = layouter.styles[i+1].offset;
831 						assert(originalOffset >= newStyleBegin);
832 						layouter.styles[i+1].offset = newStyleBegin;
833 						layouter.styles[i+1].length += originalOffset - newStyleBegin;
834 
835 						// i will NOT change the style info index yet, since the next iteration will do that
836 						continue;
837 					} else {
838 						// at the end of the loop we can just append the new thing and break out of here
839 						layouter.styles ~= TextLayouter.StyleBlock(newStyleBegin, newStyleEnd - newStyleBegin, style.index);
840 						break;
841 					}
842 				}
843 				else {
844 					// this should be impossible as i think i covered all the cases above
845 					// as we iterate through
846 					// writeln(oldStyleBegin, "..", oldStyleEnd, " -- ", newStyleBegin, "..", newStyleEnd);
847 					assert(0);
848 				}
849 			}
850 		}
851 
852 		// foreach(st; layouter.styles) writeln("A: ", st.offset, "..", st.offset + st.length, " ", st.styleInformationIndex);
853 	}
854 
855 	// returns the edge of the new cursor position
856 	private void horizontalMoveImpl(int direction, int count, bool byRender) {
857 		assert(direction != 0);
858 
859 		auto place = impl.focus + direction;
860 
861 		foreach(c; 0 .. count) {
862 			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
863 				place += direction;
864 		}
865 
866 		// 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
867 
868 		if(place < 0)
869 			place = 0;
870 		if(place >= layouter.text.length)
871 			place = cast(int) layouter.text.length - 1;
872 
873 		impl.position = place;
874 
875 	}
876 
877 	// returns the baseline of the new cursor
878 	private void verticalMoveImpl(int direction, int count, bool byRender) {
879 		assert(direction != 0);
880 		// this needs to find the closest glyph on the virtual x on the previous (rendered) line
881 
882 		int segmentIndex = layouter.findContainingSegment(impl.terminus);
883 
884 		// we know this is going to lead to a different segment since
885 		// the layout breaks up that way, so we can just go straight backward
886 
887 		auto segment = layouter.segments[segmentIndex];
888 
889 		auto idealX = impl.virtualFocusPosition.x;
890 
891 		auto targetLineNumber = segment.displayLineNumber + (direction * count);
892 		if(targetLineNumber < 0)
893 			targetLineNumber = 0;
894 
895 		// FIXME if(!byRender)
896 
897 
898 		// FIXME: when you are going down, a line that begins with tab doesn't highlight the right char.
899 
900 		int bestHit = -1;
901 		int bestHitDistance = int.max;
902 
903 		// writeln(targetLineNumber, " ", segmentIndex, " ", layouter.segments.length);
904 
905 		segmentLoop: while(segmentIndex >= 0 && segmentIndex < layouter.segments.length) {
906 			segment = layouter.segments[segmentIndex];
907 			if(segment.displayLineNumber == targetLineNumber) {
908 				// we're in the right line... but not necessarily the right segment
909 				// writeln("line found");
910 				if(idealX >= segment.boundingBox.left && idealX < segment.boundingBox.right) {
911 					// need to find the exact thing in here
912 
913 					auto hit = segment.textBeginOffset;
914 					auto ul = segment.upperLeft;
915 
916 					bool found;
917 					auto txt = layouter.text[segment.textBeginOffset .. segment.textEndOffset];
918 					auto codepoint = 0;
919 					foreach(idx, dchar d; txt) {
920 						auto width = layouter.segmentsWidths[segmentIndex][codepoint];
921 
922 						hit = segment.textBeginOffset + cast(int) idx;
923 
924 						auto distanceToLeft = ul.x - idealX;
925 						if(distanceToLeft < 0) distanceToLeft = -distanceToLeft;
926 						if(distanceToLeft < bestHitDistance) {
927 							bestHit = hit;
928 							bestHitDistance = distanceToLeft;
929 						} else {
930 							// getting further away = no help
931 							break;
932 						}
933 
934 						/*
935 						// FIXME: I probably want something slightly different
936 						if(ul.x >= idealX) {
937 							found = true;
938 							break;
939 						}
940 						*/
941 
942 						ul.x += width;
943 						codepoint++;
944 					}
945 
946 					/*
947 					if(!found)
948 						hit = segment.textEndOffset - 1;
949 
950 					impl.position = hit;
951 					bestHit = -1;
952 					*/
953 
954 					impl.position = bestHit;
955 					bestHit = -1;
956 
957 					// selections[selectionId].virtualFocusPosition = Point(selections[selectionId].virtualFocusPosition.x, segment.boundingBox.bottom);
958 
959 					break segmentLoop;
960 				} else {
961 					// FIXME: assuming ltr here
962 					auto distance = idealX - segment.boundingBox.right;
963 					if(distance < 0)
964 						distance = -distance;
965 					if(bestHit == -1 || distance < bestHitDistance) {
966 						bestHit = segment.textEndOffset - 1;
967 						bestHitDistance = distance;
968 					}
969 				}
970 			} else if(bestHit != -1) {
971 				impl.position = bestHit;
972 				bestHit = -1;
973 				break segmentLoop;
974 			}
975 
976 			segmentIndex += direction;
977 		}
978 
979 		if(bestHit != -1)
980 			impl.position = bestHit;
981 
982 		if(impl.position == layouter.text.length)
983 			impl.position -- ; // never select the eof marker
984 	}
985 }
986 
987 unittest {
988 	auto l = new TextLayouter(new class TextStyle {
989 		mixin Defaults;
990 	});
991 
992 	l.appendText("this is a test string again");
993 	auto s = l.selection();
994 	auto result = s.find(b => (b == "a") ? b : null, 1, false);
995 	assert(result.wasFound);
996 	assert(result.position == 8);
997 	assert(result.endPosition == 9);
998 	result.selectHit();
999 	assert(s.getContentString() == "a");
1000 	result.moveToEnd();
1001 	result = s.find(b => (b == "a") ? b : null, 1, false); // should find next
1002 	assert(result.wasFound);
1003 	assert(result.position == 22);
1004 	assert(result.endPosition == 23);
1005 }
1006 
1007 private struct SelectionImpl {
1008 	// you want multiple selections at most points
1009 	int id;
1010 	int anchor;
1011 	int terminus;
1012 
1013 	int position;
1014 
1015 	alias focus = terminus;
1016 
1017 	/+
1018 		As you move through lines of different lengths, your actual x will change,
1019 		but the user will want to stay in the same relative spot, consider passing:
1020 
1021 		long thing
1022 		short
1023 		long thing
1024 
1025 		from the 'i'. When you go down, you'd be back by the t, but go down again, you should
1026 		go back to the i. This variable helps achieve this.
1027 	+/
1028 	Point virtualFocusPosition;
1029 
1030 	int start() {
1031 		return anchor <= terminus ? anchor : terminus;
1032 	}
1033 	int end() {
1034 		return anchor <= terminus ? terminus : anchor;
1035 	}
1036 	bool empty() {
1037 		return anchor == terminus;
1038 	}
1039 	bool containsOffset(int textOffset) {
1040 		return textOffset >= start && textOffset < end;
1041 	}
1042 	bool isIncludedInRange(int textStart, int textEnd) {
1043 		// if either end are in there, we're obviously in the range
1044 		if((start >= textStart && start < textEnd) || (end >= textStart && end < textEnd))
1045 			return true;
1046 		// or if the selection is entirely inside the given range...
1047 		if(start >= textStart && end < textEnd)
1048 			return true;
1049 		// or if the given range is at all inside the selection
1050 		if((textStart >= start && textStart < end) || (textEnd >= start && textEnd < end))
1051 			return true;
1052 		return false;
1053 	}
1054 }
1055 
1056 /++
1057 	Bugs:
1058 		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.
1059 +/
1060 class TextLayouter {
1061 
1062 
1063 	// actually running this invariant gives quadratic performance in the layouter (cuz of isWordwrapPoint lol)
1064 	// so gonna only version it in for special occasions
1065 	version(none)
1066 	invariant() {
1067 		// There is one and exactly one segment for every char in the string.
1068 		// The segments are stored in sequence from index 0 to the end of the string.
1069 		// styleInformationIndex is always in bounds of the styles array.
1070 		// There is one and exactly one style block for every char in the string.
1071 		// Style blocks are stored in sequence from index 0 to the end of the string.
1072 
1073 		assert(text.length > 0 && text[$-1] == 0);
1074 		assert(styles.length >= 1);
1075 		int last = 0;
1076 		foreach(style; styles) {
1077 			assert(style.offset == last); // all styles must be in order and contiguous
1078 			assert(style.length > 0); // and non-empty
1079 			assert(style.styleInformationIndex != -1); // and not default constructed (this should be resolved before adding)
1080 			assert(style.styleInformationIndex >= 0 && style.styleInformationIndex < stylePalette.length); // all must be in-bounds
1081 			last = style.offset + style.length;
1082 		}
1083 		assert(last == text.length); // and all chars in the array must be covered by a style block
1084 	}
1085 
1086 	/+
1087 	private void notifySelectionChanged() {
1088 		if(onSelectionChanged !is null)
1089 			onSelectionChanged(this);
1090 	}
1091 
1092 	/++
1093 		A delegate called when the current selection is changed through api or user action.
1094 
1095 		History:
1096 			Added July 10, 2024
1097 	+/
1098 	void delegate(TextLayouter l) onSelectionChanged;
1099 	+/
1100 
1101 	/++
1102 		Gets the object representing the given selection.
1103 
1104 		Normally, id = 0 is the user's selection, then id's 60, 61, 62, and 63 are private to the application.
1105 	+/
1106 	Selection selection(int id = 0) {
1107 		assert(id >= 0 && id < _selections.length);
1108 		return Selection(this, id);
1109 	}
1110 
1111 	/++
1112 		The rendered size of the text.
1113 	+/
1114 	public int width() {
1115 		relayoutIfNecessary();
1116 		return _width;
1117 	}
1118 
1119 	/// ditto
1120 	public int height() {
1121 		relayoutIfNecessary();
1122 		return _height;
1123 	}
1124 
1125 	static struct State {
1126 		// for the delta compression, the text is the main field to worry about
1127 		// and what it really needs to know is just based on what, then what is added and what is removed.
1128 		// i think everything else i'd just copy in (or reference the same array) anyway since they're so
1129 		// much smaller anyway.
1130 		//
1131 		// and if the text is small might as well just copy/reference it too tbh.
1132 		private {
1133 			char[] text;
1134 			TextStyle[] stylePalette;
1135 			StyleBlock[] styles;
1136 			SelectionImpl[] selections;
1137 		}
1138 	}
1139 
1140 	// for manual undo stuff
1141 	// and all state should be able to do do it incrementally too; each modification to those should be compared.
1142 	/++
1143 		The editor's internal state can be saved and restored as an opaque blob. You might use this to make undo checkpoints and similar.
1144 
1145 		Its implementation may use delta compression from a previous saved state, it will try to do this transparently for you to save memory.
1146 	+/
1147 	const(State)* saveState() {
1148 		return new State(text.dup, stylePalette.dup, styles.dup, _selections.dup);
1149 	}
1150 	/// ditto
1151 	void restoreState(const(State)* state) {
1152 		auto changeInLength = cast(int) this.text.length - cast(int) state.text.length;
1153 		this.text = state.text.dup;
1154 		// FIXME: bad cast
1155 		this.stylePalette = (cast(TextStyle[]) state.stylePalette).dup;
1156 		this.styles = state.styles.dup;
1157 		this._selections = state.selections.dup;
1158 
1159 		invalidateLayout(0, text.length, changeInLength);
1160 	}
1161 
1162 	// FIXME: I might want to make the original line number exposed somewhere too like in the segment draw information
1163 
1164 	// FIXME: all the actual content - styles, text, and selection stuff - needs to be able to communicate its changes
1165 	// incrementally for the network use case. the segments tho not that important.
1166 
1167 	// FIXME: for the password thing all the glyph positions need to be known to this system, so it can't just draw it
1168 	// 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)
1169 	// so actually that is the magic lol
1170 
1171 	private static struct StyleBlock {
1172 		int offset;
1173 		int length;
1174 
1175 		int styleInformationIndex;
1176 	}
1177 
1178 	void resetSelection(int selectionId) {
1179 
1180 	}
1181 
1182 	// FIXME: is it moving teh anchor or the focus?
1183 	void extendSelection(int selectionId, bool fromBeginning, bool direction, int delegate(scope const char[] c) handler) {
1184 		// iterates through the selection, giving you the chars, until you return 0
1185 		// can use this to do things like delete words in front of cursor
1186 	}
1187 
1188 	void duplicateSelection(int receivingSelectionId, int sourceSelectionId) {
1189 
1190 	}
1191 
1192 	private int findContainingSegment(int textOffset) {
1193 
1194 		relayoutIfNecessary();
1195 
1196 		// FIXME: binary search
1197 
1198 		// FIXME: when the index is here, use it
1199 		foreach(idx, segment; segments) {
1200 			// this assumes the segments are in order of text offset
1201 			if(textOffset >= segment.textBeginOffset && textOffset < segment.textEndOffset)
1202 				return cast(int) idx;
1203 		}
1204 		assert(0);
1205 	}
1206 
1207 	// need page up+down, home, edit, arrows, etc.
1208 
1209 	/++
1210 		Finds the given text, setting the given selection to it, if found.
1211 
1212 		Starts from the given selection and moves in the direction to find next.
1213 
1214 		Returns true if found.
1215 
1216 		NOT IMPLEMENTED use a selection instead
1217 	+/
1218 	FindResult find(int selectionId, in const(char)[] text, bool direction, bool wraparound) {
1219 		return FindResult.NotFound;
1220 	}
1221 	/// ditto
1222 	enum FindResult : int {
1223 		NotFound = 0,
1224 		Found = 1,
1225 		WrappedAround = 2
1226 	}
1227 
1228 	private bool wasMutated_ = false;
1229 	/++
1230 		The layouter maintains a flag to tell if the content has been changed.
1231 	+/
1232 	public bool wasMutated() {
1233 		return wasMutated_;
1234 	}
1235 
1236 	/// ditto
1237 	public void clearWasMutatedFlag() {
1238 		wasMutated_ = false;
1239 	}
1240 
1241 	/++
1242 		Represents a possible registered style for a segment of text.
1243 	+/
1244 	public static struct StyleHandle {
1245 		private this(int idx) { this.index = idx; }
1246 		private int index = -1;
1247 	}
1248 
1249 	/++
1250 		Registers a text style you can use in text segments.
1251 	+/
1252 	// FIXME: i might have to construct it internally myself so i can return it const.
1253 	public StyleHandle registerStyle(TextStyle style) {
1254 		stylePalette ~= style;
1255 		return StyleHandle(cast(int) stylePalette.length - 1);
1256 	}
1257 
1258 
1259 	/++
1260 		Appends text at the end, without disturbing user selection.
1261 	+/
1262 	public void appendText(scope const(char)[] text, StyleHandle style = StyleHandle.init) {
1263 		wasMutated_ = true;
1264 		auto before = this.text;
1265 		this.text.length += text.length;
1266 		this.text[before.length-1 .. before.length-1 + text.length] = text[];
1267 		this.text[$-1] = 0; // gotta maintain the zero terminator i use
1268 		// or do i want to use init to just keep using the same style as is already there?
1269 		if(style is StyleHandle.init) {
1270 			// default is to extend the existing style
1271 			styles[$-1].length += text.length;
1272 		} else {
1273 			// otherwise, insert a new block for it
1274 			styles[$-1].length -= 1; // it no longer covers the zero terminator
1275 
1276 			// but this does, hence the +1
1277 			styles ~= StyleBlock(cast(int) before.length - 1, cast(int) text.length + 1, style.index);
1278 		}
1279 
1280 		invalidateLayout(cast(int) before.length - 1 /* zero terminator */, this.text.length, cast(int) text.length);
1281 	}
1282 
1283 	/++
1284 		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.
1285 
1286 		FIXME: have a getTextInSelection
1287 
1288 		FIXME: have some kind of index stuff so you can select some text found in here (think regex search)
1289 	+/
1290 	void getText(scope void delegate(scope const(char)[] segment, TextStyle style) handler) {
1291 		handler(text[0 .. $-1], null); // cut off the null terminator
1292 	}
1293 
1294 	/++
1295 		Gets the current text value as a plain-text string.
1296 	+/
1297 	string getTextString() {
1298 		string s;
1299 		getText((segment, style) {
1300 			s ~= segment;
1301 		});
1302 		return s;
1303 	}
1304 
1305 	public static struct DrawingInformation {
1306 		Rectangle boundingBox;
1307 		Point initialBaseline;
1308 		ulong selections; // 0 if not selected. bitmask of selection ids otherwise
1309 
1310 		int direction; // you start at initialBaseline then draw ltr or rtl or up or down.
1311 		// might also store glyph id, which could be encoded texture # + position, stuff like that. if each segment were
1312 		// a glyph at least which is sometimes needed but prolly not gonna stress abut that in my use cases, i'd rather batch.
1313 	}
1314 
1315 	public static struct CaretInformation {
1316 		int id;
1317 		Rectangle boundingBox;
1318 	}
1319 
1320 	// assumes the idx is indeed in the segment
1321 	private Rectangle boundingBoxOfGlyph(size_t segmentIndex, int idx) {
1322 		// I can't relayoutIfNecessary here because that might invalidate the segmentIndex!!
1323 		// relayoutIfNecessary();
1324 		auto segment = segments[segmentIndex];
1325 
1326 		int codepointCounter = 0;
1327 		auto bb = segment.boundingBox;
1328 		foreach(thing, dchar cp; text[segment.textBeginOffset .. segment.textEndOffset]) {
1329 			auto w = segmentsWidths[segmentIndex][codepointCounter];
1330 
1331 			if(thing + segment.textBeginOffset == idx) {
1332 				bb.right = bb.left + w;
1333 				return bb;
1334 			}
1335 
1336 			bb.left += w;
1337 
1338 			codepointCounter++;
1339 		}
1340 
1341 		bb.right = bb.left + 1;
1342 
1343 		return bb;
1344 	}
1345 
1346 	void getTextAtPosition(Point p) {
1347 		relayoutIfNecessary();
1348 		// 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)
1349 		// then might want dropTextAt(Point p)
1350 	}
1351 
1352 	/++
1353 		Gets the text that you need to draw, guaranteeing each call to your delegate will:
1354 
1355 		* Have a contiguous slice into text
1356 		* 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.)
1357 		* Be a linear block of text that fits in a single rectangular segment
1358 		* 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.
1359 		* The segment may be a special escape sequence. FIXME explain how to check this.
1360 
1361 		Return `false` from your delegate to stop iterating through the text.
1362 
1363 		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.
1364 
1365 		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.
1366 
1367 		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.
1368 	+/
1369 	void getDrawableText(scope bool delegate(scope const(char)[] segment, TextStyle style, DrawingInformation information, CaretInformation[] carets...) dg, Rectangle box = Rectangle.init) {
1370 		relayoutIfNecessary();
1371 		getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
1372 			if(segment.textBeginOffset == -1)
1373 				return true;
1374 
1375 			TextStyle style;
1376 			assert(segment.styleInformationIndex < stylePalette.length);
1377 
1378 			style = stylePalette[segment.styleInformationIndex];
1379 
1380 			ubyte[64] possibleSelections;
1381 			int possibleSelectionsCount;
1382 
1383 			CaretInformation[64] caretInformation;
1384 			int cic;
1385 
1386 			// bounding box reduction
1387 			foreach(si, selection; _selections[0 .. selectionsInUse]) {
1388 				if(selection.isIncludedInRange(segment.textBeginOffset, segment.textEndOffset)) {
1389 					if(!selection.empty()) {
1390 						possibleSelections[possibleSelectionsCount++] = cast(ubyte) si;
1391 					}
1392 					if(selection.focus >= segment.textBeginOffset && selection.focus < segment.textEndOffset) {
1393 
1394 						// make sure the caret box represents that it would be if we actually
1395 						// did the insertion, so adjust the bounding box to account for a possibly
1396 						// different font
1397 
1398 						auto insertionStyle = stylePalette[getInsertionStyleAt(selection.focus).index];
1399 						auto glyphStyle = style;
1400 
1401 						auto bb = boundingBoxOfGlyph(segmentIndex, selection.focus);
1402 
1403 						bb.top += glyphStyle.font.ascent;
1404 						bb.bottom -= glyphStyle.font.descent;
1405 
1406 						bb.top -= insertionStyle.font.ascent;
1407 						bb.bottom += insertionStyle.font.descent;
1408 
1409 						caretInformation[cic++] = CaretInformation(cast(int) si, bb);
1410 					}
1411 				}
1412 			}
1413 
1414 			// the rest of this might need splitting based on selections
1415 
1416 			DrawingInformation di;
1417 			di.boundingBox = Rectangle(segment.upperLeft, Size(segment.width, segment.height));
1418 			di.selections = 0;
1419 
1420 			// di.initialBaseline = Point(x, y); // FIXME
1421 			// FIXME if the selection focus is in this box, we should set the caretPosition to the bounding box of the associated glyph
1422 			// di.caretPosition = Rectangle(x, y, w, h); // FIXME
1423 
1424 			auto end = segment.textEndOffset;
1425 			if(end == text.length)
1426 				end--; // don't send the terminating 0 to the user as that's an internal detail
1427 
1428 			auto txt = text[segment.textBeginOffset .. end];
1429 
1430 			if(possibleSelectionsCount == 0) {
1431 				// no selections present, no need to iterate
1432 				// FIXME: but i might have to take some gap chars and such out anyway.
1433 				return dg(txt, style, di, caretInformation[0 .. cic]);
1434 			} else {
1435 				ulong lastSel = 0;
1436 				size_t lastSelPos = 0;
1437 				size_t lastSelCodepoint = 0;
1438 				bool exit = false;
1439 
1440 				void sendSegment(size_t start, size_t end, size_t codepointStart, size_t codepointEnd) {
1441 					di.selections = lastSel;
1442 
1443 					Rectangle bbOriginal = di.boundingBox;
1444 
1445 					int segmentWidth;
1446 
1447 					foreach(width; segmentsWidths[segmentIndex][codepointStart .. codepointEnd]) {
1448 						segmentWidth += width;
1449 					}
1450 
1451 					auto diFragment = di;
1452 					diFragment.boundingBox.right = diFragment.boundingBox.left + segmentWidth;
1453 
1454 					// FIXME: adjust the rest of di for this
1455 					// FIXME: the caretInformation arguably should be truncated for those not in this particular sub-segment
1456 					exit = !dg(txt[start .. end], style, diFragment, caretInformation[0 .. cic]);
1457 
1458 					di.initialBaseline.x += segmentWidth;
1459 					di.boundingBox.left += segmentWidth;
1460 
1461 					lastSelPos = end;
1462 					lastSelCodepoint = codepointEnd;
1463 				}
1464 
1465 				size_t codepoint = 0;
1466 
1467 				foreach(ci, dchar ch; txt) {
1468 					auto sel = selectionsAt(cast(int) ci + segment.textBeginOffset);
1469 					if(sel != lastSel) {
1470 						// send this segment
1471 
1472 						sendSegment(lastSelPos, ci, lastSelCodepoint, codepoint);
1473 						lastSel = sel;
1474 						if(exit) return false;
1475 					}
1476 
1477 					codepoint++;
1478 				}
1479 
1480 				sendSegment(lastSelPos, txt.length, lastSelCodepoint, codepoint);
1481 				if(exit) return false;
1482 			}
1483 
1484 			return true;
1485 		}, box);
1486 	}
1487 
1488 	// 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
1489 	// may return more than is necessary; it uses the box as a hint to speed the search, not as the strict bounds it returns.
1490 	void getInternalSegments(scope bool delegate(size_t idx, scope ref Segment segment) dg, Rectangle box = Rectangle.init) {
1491 		relayoutIfNecessary();
1492 
1493 		if(box.right == box.left)
1494 			box.right = int.max;
1495 		if(box.bottom == box.top)
1496 			box.bottom = int.max;
1497 
1498 		if(segments.length < 64 || box.top < 64) {
1499 			foreach(idx, ref segment; segments) {
1500 				if(dg(idx, segment) == false)
1501 					break;
1502 			}
1503 		} else {
1504 			int maximum = cast(int) segments.length;
1505 			int searchPoint = maximum / 2;
1506 
1507 			keepSearching:
1508 			//writeln(searchPoint);
1509 			if(segments[searchPoint].upperLeft.y > box.top) {
1510 				// we're too far ahead to find the box
1511 				maximum = searchPoint;
1512 				auto newSearchPoint = maximum / 2;
1513 				if(newSearchPoint == searchPoint) {
1514 					searchPoint = newSearchPoint;
1515 					goto useIt;
1516 				}
1517 				searchPoint = newSearchPoint;
1518 				goto keepSearching;
1519 			} else if(segments[searchPoint].boundingBox.bottom < box.top) {
1520 				// the box is a way down from here still
1521 				auto newSearchPoint = (maximum - searchPoint) / 2 + searchPoint;
1522 				if(newSearchPoint == searchPoint) {
1523 					searchPoint = newSearchPoint;
1524 					goto useIt;
1525 				}
1526 				searchPoint = newSearchPoint;
1527 				goto keepSearching;
1528 			}
1529 
1530 			useIt:
1531 
1532 			auto line = segments[searchPoint].displayLineNumber;
1533 			if(line) {
1534 				// go to the line right before this to ensure we have everything in here
1535 				while(searchPoint != 0 && segments[searchPoint].displayLineNumber == line)
1536 					searchPoint--;
1537 			}
1538 
1539 			foreach(idx, ref segment; segments[searchPoint .. $]) {
1540 				if(dg(idx + searchPoint, segment) == false)
1541 					break;
1542 			}
1543 		}
1544 	}
1545 
1546 	private {
1547 		// user code can add new styles to the palette
1548 		TextStyle[] stylePalette;
1549 
1550 		// if editable by user, these will change
1551 		char[] text;
1552 		StyleBlock[] styles;
1553 
1554 		// the layout function calculates these
1555 		Segment[] segments;
1556 		short[][] segmentsWidths;
1557 	}
1558 
1559 	/++
1560 
1561 	+/
1562 	this(TextStyle defaultStyle) {
1563 		this.stylePalette ~= defaultStyle;
1564 		this.text = [0]; // i never want to let the editor go over, so this pseudochar makes that a bit easier
1565 		this.styles ~= StyleBlock(0, 1, 0); // default style should never be deleted too at the end of the file
1566 		this.invalidateLayout(0, 1, 0);
1567 	}
1568 
1569 	// maybe unstable
1570 	TextStyle defaultStyle() {
1571 		auto ts = this.stylePalette[0];
1572 		invalidateLayout(0, text.length, 0); // assume they are going to mutate it
1573 		return ts;
1574 	}
1575 
1576 	bool editable;
1577 	int wordWrapLength = 0;
1578 	int delegate(int x) tabStop = null;
1579 	int delegate(Rectangle line) leftOffset = null;
1580 	int delegate(Rectangle line) rightOffset = null;
1581 	int lineSpacing = 0;
1582 
1583 	/+
1584 		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.
1585 
1586 		it takes a flag: UpperLeft or Baseline. this tells its coordinates for the string segment when you draw.
1587 
1588 		The style can just be a void* or something, not really the problem of the layouter; it only cares about font metrics
1589 
1590 		The layout thing needs to know:
1591 			1) is it word wrapped
1592 			2) a delegate for offset left for the given line height
1593 			2) a delegate for offset right for the given line height
1594 
1595 		GetSelection() returns the segments that are currently selected
1596 		Caret position, if there is one
1597 
1598 		Each TextLayouter can represent a block element in HTML terms. Centering and such done outside.
1599 		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).
1600 
1601 
1602 		EDITING:
1603 			just like addText which it does replacing the selection if there is one or inserting/overstriking at the caret
1604 
1605 			everything has an optional void* style which it does as offset-based overlays
1606 
1607 			user responsibility to getSelection if they want to add something to the style
1608 	+/
1609 
1610 	private static struct Segment {
1611 		// 32 bytes rn, i can reasonably save 6 with shorts
1612 		// do i even need the segmentsWidths cache or can i reasonably recalculate it lazily?
1613 
1614 		int textBeginOffset;
1615 		int textEndOffset; // can make a short length i think
1616 
1617 		int styleInformationIndex;
1618 
1619 		// calculated values after iterating through the segment
1620 		int width; // short
1621 		int height; // short
1622 
1623 		Point upperLeft;
1624 
1625 		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
1626 
1627 		/*
1628 		Point baseline() {
1629 
1630 		}
1631 		*/
1632 
1633 		Rectangle boundingBox() {
1634 			return Rectangle(upperLeft, Size(width, height));
1635 		}
1636 	}
1637 
1638 	private int _width;
1639 	private int _height;
1640 
1641 	private SelectionImpl[64] _selections;
1642 	private int selectionsInUse = 1;
1643 
1644 	/++
1645 		Selections have two parts: an anchor (typically set to where the user clicked the mouse down)
1646 		and a focus (typically where the user released the mouse button). As you click and drag, you
1647 		want to change the focus while keeping the anchor the same.
1648 
1649 		The caret is drawn at the focus. If the anchor and focus are the same point, the selection
1650 		is empty.
1651 
1652 		Please note that the selection focus is different than a keyboard focus. (I'd personally prefer
1653 		to call it a terminus, but I'm trying to use the same terminology as the web standards, even if
1654 		I don't like it.)
1655 
1656 		After calling this, you don't need to call relayout(), but you might want to redraw to show the
1657 		user the result of this action.
1658 	+/
1659 
1660 	// FIXME: the public one might be like segmentOfClick so you can get the style info out (which might hold hyperlink data)
1661 
1662 	/+
1663 		Returns the nearest offset in the text for the given point.
1664 
1665 		it should return if it was inside the segment bounding box tho
1666 	+/
1667 	int offsetOfClick(Point p) {
1668 		int idx = cast(int) text.length - 1;
1669 
1670 		relayoutIfNecessary();
1671 
1672 		if(p.y > _height)
1673 			return idx;
1674 
1675 		getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
1676 			idx = segment.textBeginOffset;
1677 			// FIXME: this all assumes ltr
1678 
1679 			auto boundingBox = Rectangle(segment.upperLeft, Size(segment.width, segment.height));
1680 			if(boundingBox.contains(p)) {
1681 				int x = segment.upperLeft.x;
1682 				int codePointIndex = 0;
1683 
1684 				int bestHit = int.max;
1685 				int bestHitDistance = int.max;
1686 				if(bestHitDistance < 0) bestHitDistance = -bestHitDistance;
1687 				foreach(i, dchar ch; text[segment.textBeginOffset .. segment.textEndOffset]) {
1688 					const width = segmentsWidths[segmentIndex][codePointIndex];
1689 					idx = segment.textBeginOffset + cast(int) i; // can't just idx++ since it needs utf-8 stride
1690 
1691 					auto distanceToLeft = p.x - x;
1692 					if(distanceToLeft < 0) distanceToLeft = -distanceToLeft;
1693 
1694 					//auto distanceToRight = p.x - (x + width);
1695 					//if(distanceToRight < 0) distanceToRight = -distanceToRight;
1696 
1697 					//bool improved = false;
1698 
1699 					if(distanceToLeft < bestHitDistance) {
1700 						bestHit = idx;
1701 						bestHitDistance = distanceToLeft;
1702 						// improved = true;
1703 					}
1704 					/*
1705 					if(distanceToRight < bestHitDistance) {
1706 						bestHit = idx + 1;
1707 						bestHitDistance = distanceToRight;
1708 						improved = true;
1709 					}
1710 					*/
1711 
1712 					//if(!improved) {
1713 						// we're moving further away, no point continuing
1714 						// (please note that RTL transitions = different segment)
1715 						//break;
1716 					//}
1717 
1718 					x += width;
1719 					codePointIndex++;
1720 				}
1721 
1722 				if(bestHit != int.max)
1723 					idx = bestHit;
1724 
1725 				return false;
1726 			} else if(p.x < boundingBox.left && p.y >= boundingBox.top && p.y < boundingBox.bottom) {
1727 				// to the left of a line
1728 				// assumes ltr
1729 				idx = segment.textBeginOffset;
1730 				return false;
1731 			/+
1732 			} else if(p.x >= boundingBox.right && p.y >= boundingBox.top && p.y < boundingBox.bottom) {
1733 				// to the right of a line
1734 				idx = segment.textEndOffset;
1735 				return false;
1736 			+/
1737 			} else if(p.y < segment.upperLeft.y) {
1738 				// should go to the end of the previous line
1739 				auto thisLine = segment.displayLineNumber;
1740 				idx = 0;
1741 				while(segmentIndex > 0) {
1742 					segmentIndex--;
1743 
1744 					if(segments[segmentIndex].displayLineNumber < thisLine) {
1745 						idx = segments[segmentIndex].textEndOffset - 1;
1746 						break;
1747 					}
1748 				}
1749 				return false;
1750 			} else {
1751 				// for single line if nothing else matched we'd best put it at the end; will be reset for the next iteration
1752 				// if there is one. and if not, this is where we want it - at the end of the text
1753 				idx = cast(int) text.length - 1;
1754 			}
1755 
1756 			return true;
1757 		}, Rectangle(p, Size(0, 0)));
1758 		return idx;
1759 	}
1760 
1761 	/++
1762 
1763 		History:
1764 			Added September 13, 2024
1765 	+/
1766 	const(TextStyle) styleAtPoint(Point p) {
1767 		TextStyle s;
1768 		getInternalSegments(delegate bool(size_t segmentIndex, scope ref Segment segment) {
1769 			if(segment.boundingBox.contains(p)) {
1770 				s = stylePalette[segment.styleInformationIndex];
1771 				return false;
1772 			}
1773 
1774 			return true;
1775 		}, Rectangle(p, Size(1, 1)));
1776 
1777 		return s;
1778 	}
1779 
1780 	private StyleHandle getInsertionStyleAt(int offset) {
1781 		assert(offset >= 0 && offset < text.length);
1782 		/+
1783 			If we are at the first part of a logical line, use the next local style (the one in bounds at the offset).
1784 
1785 			Otherwise, use the previous one (the one in bounds).
1786 		+/
1787 
1788 		if(offset == 0 || text[offset - 1] == '\n') {
1789 			// no adjust needed, we use the style here
1790 		} else {
1791 			offset--; // use the previous one
1792 		}
1793 
1794 		return getStyleAt(offset);
1795 	}
1796 
1797 	private StyleHandle getStyleAt(int offset) {
1798 		// FIXME: binary search
1799 		foreach(style; styles) {
1800 			if(offset >= style.offset && offset < (style.offset + style.length))
1801 				return StyleHandle(style.styleInformationIndex);
1802 		}
1803 		assert(0);
1804 	}
1805 
1806 	ulong selectionsAt(int offset) {
1807 		ulong result;
1808 		ulong bit = 1;
1809 		foreach(selection; _selections[0 .. selectionsInUse]) {
1810 			if(selection.containsOffset(offset))
1811 				result |= bit;
1812 			bit <<= 1;
1813 		}
1814 		return result;
1815 	}
1816 
1817 	private int wordWrapWidth_;
1818 
1819 	/++
1820 		Set to 0 to disable word wrapping.
1821 	+/
1822 	public void wordWrapWidth(int width) {
1823 		if(width != wordWrapWidth_) {
1824 			wordWrapWidth_ = width;
1825 			invalidateLayout(0, text.length, 0);
1826 		}
1827 	}
1828 
1829 	private int justificationWidth_;
1830 
1831 	/++
1832 
1833 	+/
1834 	public void justificationWidth(int width) {
1835 		if(width != justificationWidth_) {
1836 			justificationWidth_ = width;
1837 			invalidateLayout(0, text.length, 0);
1838 		}
1839 	}
1840 
1841 	protected bool isWordwrapPoint(dchar c) {
1842 		if(c == ' ')
1843 			return true;
1844 		return false;
1845 	}
1846 
1847 	private bool invalidateLayout_;
1848 	private int invalidStart = int.max;
1849 	private int invalidEnd = 0;
1850 	private int invalidatedChangeInTextLength = 0;
1851 	/++
1852 		This should be called (internally, end users don't need to see it) any time the text or style has changed.
1853 	+/
1854 	protected void invalidateLayout(size_t start, size_t end, int changeInTextLength) {
1855 		invalidateLayout_ = true;
1856 
1857 		if(start < invalidStart)
1858 			invalidStart = cast(int) start;
1859 		if(end > invalidEnd)
1860 			invalidEnd = cast(int) end;
1861 
1862 		invalidatedChangeInTextLength += changeInTextLength;
1863 	}
1864 
1865 	/++
1866 		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.
1867 	+/
1868 	protected void relayoutIfNecessary() {
1869 		if(invalidateLayout_) {
1870 			relayoutImplementation();
1871 			invalidateLayout_ = false;
1872 			invalidStart = int.max;
1873 			invalidEnd = 0;
1874 			invalidatedChangeInTextLength = 0;
1875 		}
1876 	}
1877 
1878 	/++
1879 		Params:
1880 			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.
1881 
1882 			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.
1883 
1884 			substituteGlyph?  actually that can prolly be a fake password font.
1885 
1886 
1887 			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)
1888 	+/
1889 	protected void relayoutImplementation() {
1890 
1891 
1892 		// an optimization here is to avoid redoing stuff outside the invalidated zone.
1893 		// basically it would keep going until a segment after the invalidated end area was in the state before and after.
1894 
1895 		debug(text_layouter_bench) {
1896 			// writeln("relayouting");
1897 			import core.time;
1898 			auto start = MonoTime.currTime;
1899 			scope(exit) {
1900 				writeln(MonoTime.currTime - start);
1901 			}
1902 		}
1903 
1904 		auto originalSegments = segments;
1905 		auto originalWidth = _width;
1906 		auto originalHeight = _height;
1907 		auto originalSegmentsWidths = segmentsWidths;
1908 
1909 		_width = 0;
1910 		_height = 0;
1911 
1912 		assert(invalidStart != int.max);
1913 		assert(invalidStart >= 0);
1914 		assert(invalidStart < text.length);
1915 
1916 		if(invalidEnd > text.length)
1917 			invalidEnd = cast(int) text.length;
1918 
1919 		int firstInvalidSegment = 0;
1920 
1921 		Point currentCorner = Point(0, 0);
1922 		int displayLineNumber = 0;
1923 		int lineSegmentIndexStart = 0;
1924 
1925 		if(invalidStart != 0) {
1926 			// while i could binary search for the invalid thing,
1927 			// i also need to rebuild _width and _height anyway so
1928 			// just gonna loop through and hope for the best.
1929 			bool found = false;
1930 
1931 			// I can't just use the segment bounding box per se since that isn't the whole line
1932 			// and the finishLine adjustment for mixed fonts/sizes will throw things off. so we
1933 			// want to start at the very corner of the line
1934 			int lastLineY;
1935 			int thisLineY;
1936 			foreach(idx, segment; segments) {
1937 				// FIXME: i might actually need to go back to the logical line
1938 				if(displayLineNumber != segment.displayLineNumber) {
1939 					lastLineY = thisLineY;
1940 					displayLineNumber = segment.displayLineNumber;
1941 					lineSegmentIndexStart = cast(int) idx;
1942 				}
1943 				auto b = segment.boundingBox.bottom;
1944 				if(b > thisLineY)
1945 					thisLineY = b;
1946 
1947 				if(invalidStart >= segment.textBeginOffset  && invalidStart < segment.textEndOffset) {
1948 					// we'll redo the whole line with the invalidated region since it might have other coordinate things
1949 
1950 					segment = segments[lineSegmentIndexStart];
1951 
1952 					firstInvalidSegment = lineSegmentIndexStart;// cast(int) idx;
1953 					invalidStart = segment.textBeginOffset;
1954 					displayLineNumber = segment.displayLineNumber;
1955 					currentCorner = segment.upperLeft;
1956 					currentCorner.y = lastLineY;
1957 
1958 					found = true;
1959 					break;
1960 				}
1961 
1962 				// FIXME: since we rewind to the line segment start above this might not be correct anymore.
1963 				auto bb = segment.boundingBox;
1964 				if(bb.right > _width)
1965 					_width = bb.right;
1966 				if(bb.bottom > _height)
1967 					_height = bb.bottom;
1968 			}
1969 			assert(found);
1970 		}
1971 
1972 		// writeln(invalidStart, " starts segment ", firstInvalidSegment, " and line ", displayLineNumber, " seg ", lineSegmentIndexStart);
1973 
1974 		segments = segments[0 .. firstInvalidSegment];
1975 		segments.assumeSafeAppend();
1976 
1977 		segmentsWidths = segmentsWidths[0 .. firstInvalidSegment];
1978 		segmentsWidths.assumeSafeAppend();
1979 
1980 		version(try_kerning_hack) {
1981 			size_t previousIndex = 0;
1982 			int lastWidth;
1983 			int lastWidthDistance;
1984 		}
1985 
1986 		Segment segment;
1987 
1988 		Segment previousOldSavedSegment;
1989 		short[] previousOldSavedWidths;
1990 		TextStyle currentStyle = null;
1991 		int currentStyleIndex = 0;
1992 		MeasurableFont font;
1993 		ubyte[128] glyphWidths;
1994 		void loadNewFont(MeasurableFont what) {
1995 			font = what;
1996 
1997 			// caching the ascii widths locally can give a boost to ~ 20% of the speed of this function
1998 			foreach(char c; 32 .. 128) {
1999 				auto w = font.stringWidth((&c)[0 .. 1]);
2000 				glyphWidths[c] = cast(ubyte) w; // FIXME: what if it doesn't fit?
2001 			}
2002 		}
2003 
2004 		auto styles = this.styles;
2005 
2006 		foreach(style; this.styles) {
2007 			if(invalidStart >= style.offset && invalidStart < (style.offset + style.length)) {
2008 				currentStyle = stylePalette[style.styleInformationIndex];
2009 				if(currentStyle !is null)
2010 					loadNewFont(currentStyle.font);
2011 				currentStyleIndex = style.styleInformationIndex;
2012 
2013 				styles = styles[1 .. $];
2014 				break;
2015 			} else if(style.offset > invalidStart) {
2016 				break;
2017 			}
2018 			styles = styles[1 .. $];
2019 		}
2020 
2021 		int offsetToNextStyle = int.max;
2022 		if(styles.length) {
2023 			offsetToNextStyle = styles[0].offset;
2024 		}
2025 
2026 
2027 		assert(offsetToNextStyle >= 0);
2028 
2029 		short[] widths;
2030 
2031 		size_t segmentBegan = invalidStart;
2032 		void finishSegment(size_t idx) {
2033 			if(idx == segmentBegan)
2034 				return;
2035 			segmentBegan = idx;
2036 			segment.textEndOffset = cast(int) idx;
2037 			segment.displayLineNumber = displayLineNumber;
2038 
2039 			if(segments.length < originalSegments.length) {
2040 				previousOldSavedSegment = originalSegments[segments.length];
2041 				previousOldSavedWidths = originalSegmentsWidths[segmentsWidths.length];
2042 			} else {
2043 				previousOldSavedSegment = Segment.init;
2044 				previousOldSavedWidths = null;
2045 			}
2046 
2047 			segments ~= segment;
2048 			segmentsWidths ~= widths;
2049 
2050 			segment = Segment.init;
2051 			segment.upperLeft = currentCorner;
2052 			segment.styleInformationIndex = currentStyleIndex;
2053 			segment.textBeginOffset = cast(int) idx;
2054 			widths = null;
2055 		}
2056 
2057 		// FIXME: when we start in an invalidated thing this is not necessarily right, it should be calculated above
2058 		int biggestDescent = font.descent;
2059 		int lineHeight = font.height;
2060 
2061 		bool finishLine(size_t idx, MeasurableFont outerFont) {
2062 			if(segment.textBeginOffset == idx)
2063 				return false; // no need to keep nothing.
2064 
2065 			if(currentCorner.x > this._width)
2066 				this._width = currentCorner.x;
2067 
2068 			auto thisLineY = currentCorner.y;
2069 
2070 			auto thisLineHeight = lineHeight;
2071 			currentCorner.y += lineHeight;
2072 			currentCorner.x = 0;
2073 
2074 			finishSegment(idx); // i use currentCorner in there! so this must be after that
2075 			displayLineNumber++;
2076 
2077 			lineHeight = outerFont.height;
2078 			biggestDescent = outerFont.descent;
2079 
2080 			// go back and adjust all the segments on this line to have the right height and do vertical alignment with the baseline
2081 			foreach(ref seg; segments[lineSegmentIndexStart .. $]) {
2082 				MeasurableFont font;
2083 				if(seg.styleInformationIndex < stylePalette.length) {
2084 					auto si = stylePalette[seg.styleInformationIndex];
2085 					if(si)
2086 						font = si.font;
2087 				}
2088 
2089 				auto baseline = thisLineHeight - biggestDescent;
2090 
2091 				seg.upperLeft.y += baseline - font.ascent;
2092 				seg.height = thisLineHeight - (baseline - font.ascent);
2093 			}
2094 
2095 			// now need to check if we can finish relayout early
2096 
2097 			// if we're beyond the invalidated section and have original data to compare against...
2098 			previousOldSavedSegment.textBeginOffset += invalidatedChangeInTextLength;
2099 			previousOldSavedSegment.textEndOffset += invalidatedChangeInTextLength;
2100 
2101 			/+
2102 			// FIXME: would be nice to make this work somehow - when you input a new line it needs to just adjust the y stuff
2103 			// part of the problem is that it needs to inject a new segment for the newline and then the whole old array is
2104 			// broken.
2105 			int deltaY;
2106 			int deltaLineNumber;
2107 
2108 			if(idx >= invalidEnd && segments[$-1] != previousOldSavedSegment) {
2109 				deltaY = thisLineHeight;
2110 				deltaLineNumber = 1;
2111 				previousOldSavedSegment.upperLeft.y += deltaY;
2112 				previousOldSavedSegment.displayLineNumber += deltaLineNumber;
2113 				writeln("trying deltaY = ", deltaY);
2114 				writeln(previousOldSavedSegment);
2115 				writeln(segments[$-1]);
2116 			}
2117 			+/
2118 
2119 			// FIXME: if the only thing that's changed is a y coordinate, adjust that too
2120 			// finishEarly();
2121 			if(idx >= invalidEnd && segments[$-1] == previousOldSavedSegment) {
2122 				if(segmentsWidths[$-1] == previousOldSavedWidths) {
2123 					// we've hit a point where nothing has changed, it is time to stop processing
2124 
2125 					foreach(ref seg; originalSegments[segments.length .. $]) {
2126 						seg.textBeginOffset += invalidatedChangeInTextLength;
2127 						seg.textEndOffset += invalidatedChangeInTextLength;
2128 
2129 						/+
2130 						seg.upperLeft.y += deltaY;
2131 						seg.displayLineNumber += deltaLineNumber;
2132 						+/
2133 
2134 						auto bb = seg.boundingBox;
2135 						if(bb.right > _width)
2136 							_width = bb.right;
2137 						if(bb.bottom > _height)
2138 							_height = bb.bottom;
2139 					}
2140 
2141 					// these refer to the same array or should anyway so hopefully this doesn't do anything.
2142 					// FIXME: confirm this isn't sucky
2143 					segments ~= originalSegments[segments.length .. $];
2144 					segmentsWidths ~= originalSegmentsWidths[segmentsWidths.length .. $];
2145 
2146 					return true;
2147 				} else {
2148 					// writeln("not matched");
2149 					// writeln(previousOldSavedWidths != segmentsWidths[$-1]);
2150 				}
2151 			}
2152 
2153 			lineSegmentIndexStart = cast(int) segments.length;
2154 
2155 			return false;
2156 		}
2157 
2158 		void finishEarly() {
2159 			// lol i did all the work before triggering this
2160 		}
2161 
2162 		segment.upperLeft = currentCorner;
2163 		segment.styleInformationIndex = currentStyleIndex;
2164 		segment.textBeginOffset = invalidStart;
2165 
2166 		bool endSegment;
2167 		bool endLine;
2168 
2169 		bool tryWordWrapOnNext;
2170 
2171 		// writeln("Prior to loop: ", MonoTime.currTime - start, " ", invalidStart);
2172 
2173 		// FIXME: i should prolly go by grapheme
2174 		foreach(idxRaw, dchar ch; text[invalidStart .. $]) {
2175 			auto idx = idxRaw + invalidStart;
2176 
2177 			version(try_kerning_hack)
2178 				lastWidthDistance++;
2179 			auto oldFont = font;
2180 			if(offsetToNextStyle == idx) {
2181 				auto oldStyle = currentStyle;
2182 				if(styles.length) {
2183 					StyleBlock currentStyleBlock = styles[0];
2184 					offsetToNextStyle += currentStyleBlock.length;
2185 					styles = styles[1 .. $];
2186 
2187 					currentStyle = stylePalette[currentStyleBlock.styleInformationIndex];
2188 					currentStyleIndex = currentStyleBlock.styleInformationIndex;
2189 				} else {
2190 					currentStyle = null;
2191 					offsetToNextStyle = int.max;
2192 				}
2193 				if(oldStyle !is currentStyle) {
2194 					if(!endLine)
2195 						endSegment = true;
2196 
2197 					loadNewFont(currentStyle.font);
2198 				}
2199 			}
2200 
2201 			if(tryWordWrapOnNext) {
2202 				int nextWordwrapPoint = cast(int) idx;
2203 				while(nextWordwrapPoint < text.length && !isWordwrapPoint(text[nextWordwrapPoint])) {
2204 					if(text[nextWordwrapPoint] == '\n')
2205 						break;
2206 					nextWordwrapPoint++;
2207 				}
2208 
2209 				if(currentCorner.x + font.stringWidth(text[idx .. nextWordwrapPoint]) >= wordWrapWidth_)
2210 					endLine = true;
2211 
2212 				tryWordWrapOnNext = false;
2213 			}
2214 
2215 			if(endSegment && !endLine) {
2216 				finishSegment(idx);
2217 				endSegment = false;
2218 			}
2219 
2220 			bool justChangedLine;
2221 			if(endLine) {
2222 				auto flr = finishLine(idx, oldFont);
2223 				if(flr)
2224 					return finishEarly();
2225 				endLine = false;
2226 				endSegment = false;
2227 				justChangedLine = true;
2228 			}
2229 
2230 			if(font !is oldFont) {
2231 				// FIXME: adjust height
2232 				if(justChangedLine || font.height > lineHeight)
2233 					lineHeight = font.height;
2234 				if(justChangedLine || font.descent > biggestDescent)
2235 					biggestDescent = font.descent;
2236 			}
2237 
2238 
2239 
2240 			int thisWidth = 0;
2241 
2242 			switch(ch) {
2243 				case 0:
2244 					goto advance;
2245 				case '\r':
2246 					goto advance;
2247 				case '\n':
2248 					/+
2249 					finishSegment(idx);
2250 					segment.textBeginOffset = cast(int) idx;
2251 
2252 					thisWidth = 0;
2253 					+/
2254 
2255 					endLine = true;
2256 					goto advance;
2257 
2258 					// FIXME: a tab at the end of a line causes the next line to indent
2259 				case '\t':
2260 					finishSegment(idx);
2261 
2262 					// a tab should be its own segment with no text
2263 					// per se
2264 
2265 					thisWidth = 48;
2266 
2267 					segment.width += thisWidth;
2268 					currentCorner.x += thisWidth;
2269 
2270 					endSegment = true;
2271 					goto advance;
2272 
2273 					//goto advance;
2274 				default:
2275 					// FIXME: i don't think the draw thing uses kerning but if it does this is wrong.
2276 
2277 					// figure out this length (it uses previous idx to get some kerning info used)
2278 					version(try_kerning_hack) {
2279 						if(lastWidthDistance == 1) {
2280 							auto width = font.stringWidth(text[previousIndex .. idx + stride(text[idx])]);
2281 							thisWidth = width - lastWidth;
2282 							// writeln(text[previousIndex .. idx + stride(text[idx])], " ", width, "-", lastWidth);
2283 						} else {
2284 							auto width = font.stringWidth(text[idx .. idx + stride(text[idx])]);
2285 							thisWidth = width;
2286 						}
2287 					} else {
2288 						if(text[idx] < 128)
2289 							thisWidth = glyphWidths[text[idx]];
2290 						else
2291 							thisWidth = font.stringWidth(text[idx .. idx + stride(text[idx])]);
2292 					}
2293 
2294 					segment.width += thisWidth;
2295 					currentCorner.x += thisWidth;
2296 
2297 					version(try_kerning_hack) {
2298 						lastWidth = thisWidth;
2299 						previousIndex = idx;
2300 						lastWidthDistance = 0;
2301 					}
2302 			}
2303 
2304 			if(wordWrapWidth_ > 0 && isWordwrapPoint(ch))
2305 				tryWordWrapOnNext = true;
2306 
2307 			// 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...
2308 
2309 			// 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.
2310 
2311 			// this would be to test if the kerning adjustments do anything. seems like the fonts
2312 			// don't care tbh but still.
2313 			// thisWidth = font.stringWidth(text[idx .. idx + stride(text[idx])]);
2314 
2315 			advance:
2316 			if(segment.textBeginOffset != -1) {
2317 				widths ~= cast(short) thisWidth;
2318 			}
2319 		}
2320 
2321 		finishLine(text.length, font);
2322 
2323 		_height = currentCorner.y;
2324 
2325 		assert(segments.length);
2326 
2327 		//return widths;
2328 
2329 		// writefln("%(%s\n%)", segments[0 .. 10]);
2330 	}
2331 
2332 	private {
2333 		int stride(char c) {
2334 			if(c < 0x80) {
2335 				return 1;
2336 			} else if(c == 0xff) {
2337 				return 1;
2338 			} else {
2339 				import core.bitop : bsr;
2340 				return 7 - bsr((~uint(c)) & 0xFF);
2341 			}
2342 		}
2343 	}
2344 }
2345 
2346 class StyledTextLayouter(StyleClass) : TextLayouter {
2347 
2348 }