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