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