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