The OpenD Programming Language

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