textutil

package
v0.0.0-...-f376afe Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: May 3, 2026 License: Apache-2.0 Imports: 15 Imported by: 0

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Draw

func Draw(bounds image.Rectangle, dst *ebiten.Image, str string, options *DrawOptions)

func FindWordBoundaries

func FindWordBoundaries(text string, idx int) (start, end int)

func FirstLineBreakPositionAndLen

func FirstLineBreakPositionAndLen(str string) (pos, length int)

func LastLineBreakPositionAndLen

func LastLineBreakPositionAndLen(str string) (pos, length int)

LastLineBreakPositionAndLen returns the position and the byte length of the last line break in str. If no line break is found, it returns (-1, 0).

func Measure

func Measure(width int, str string, autoWrap bool, face text.Face, lineHeight float64, tabWidth float64, keepTailingSpace bool, ellipsisString string) (float64, float64)

func MeasureHeight

func MeasureHeight(width int, str string, autoWrap bool, face text.Face, lineHeight float64, tabWidth float64, keepTailingSpace bool) float64

MeasureHeight is like Measure but only returns height. When width does not need to be computed, this avoids per-visual-line shaping calls and is dramatically cheaper for very long text (e.g. a multi-megabyte editor buffer).

func MeasureLogicalLine

func MeasureLogicalLine(width int, logicalLine string, autoWrap bool, face text.Face, lineHeight float64, tabWidth float64, keepTailingSpace bool, ellipsisString string) (float64, float64)

MeasureLogicalLine returns the rendered width and height of one logical line at the given width. Per-logical-line counterpart of Measure.

func MeasureLogicalLineHeight

func MeasureLogicalLineHeight(width int, logicalLine string, autoWrap bool, face text.Face, lineHeight float64, tabWidth float64, keepTailingSpace bool) float64

MeasureLogicalLineHeight returns the rendered height of one logical line at the given width. This is the per-logical-line counterpart of MeasureHeight and is used by virtualized layout to size lines one at a time without scanning the whole document.

func NextPositionOnGraphemes

func NextPositionOnGraphemes(str string, position int) int

func PrevPositionOnGraphemes

func PrevPositionOnGraphemes(str string, position int) int

func TextIndexFromPosition

func TextIndexFromPosition(p *TextIndexFromPositionParams) int

TextIndexFromPosition returns the byte offset in the rendering text closest to p.Position. When p.LineByteOffsets is supplied, the visual-line walk is localized: it starts from (p.LogicalLineIndexHint, p.VisualLineIndexHint) and steps forward (or backward) one logical line at a time until the line covering p.Position.Y is found. With the hint placed inside the viewport this costs O(visible lines) of typesetting per query, instead of the O(documentLen) full scan the sidecar-less fallback performs.

When an active IME composition splices into the rendering text, the committed-text sidecar is reused: byte/visual-line shifts derived from ComputeCompositionInfo map between committed and rendering coordinates without rebuilding the sidecar. Falls back to the unrestricted whole-document walk when the composition crosses a logical-line boundary, when no sidecar is supplied, or when the document is empty. The fallback is observationally equivalent to the fast path.

func TextIndexFromPositionInLogicalLine

func TextIndexFromPositionInLogicalLine(width int, position image.Point, logicalLine string, options *Options) int

TextIndexFromPositionInLogicalLine returns the byte offset within one logical line closest to the given position. The position's Y is relative to the top of the logical line. Counterpart of TextIndexFromPosition.

func VisualLineCountForLogicalLine

func VisualLineCountForLogicalLine(width int, logicalLine string, autoWrap bool, face text.Face, tabWidth float64, keepTailingSpace bool) int

VisualLineCountForLogicalLine returns the number of visual lines one logical line wraps into at the given width. With autoWrap off (or when the line fits) the result is always 1.

Types

type CompositionInfo

type CompositionInfo struct {
	// LineIndex is the logical-line index of the selection line.
	// Lines with index > LineIndex are "past the splice" and have
	// RenderingByteShift and RenderingYShift applied.
	LineIndex int

	// RenderingByteShift is added to a past-the-splice line's
	// committed byte offset to get its rendering byte offset. Equals
	// the composition's byte length minus the length of the committed
	// range it replaces, so it can be negative for selection-
	// replacement compositions.
	RenderingByteShift int

	// RenderingYShift is added to a past-the-splice line's committed
	// visual-Y (in pixels, top-of-line) to get its rendering visual-Y.
	// Non-zero only when AutoWrap is on and the composition causes the
	// selection line to wrap into a different number of visual sub-lines.
	RenderingYShift int
}

CompositionInfo describes how an active IME composition shifts the document layout for the visible-range slicer. The zero value is safe to pass when no composition is active: the shifts are zero, so any "past the splice" comparison the slicer makes is harmless.

func ComputeCompositionInfo

func ComputeCompositionInfo(p *CompositionInfoParams) (CompositionInfo, bool)

ComputeCompositionInfo classifies an active composition and returns info that the textutil functions use to translate between committed and rendering byte/visual-line coordinates. ok is false when the splice changes the logical-line count - a hard line break inside the composition or a selection that straddles a logical line boundary - and the caller should fall back to drawing the unrestricted text.

type CompositionInfoParams

type CompositionInfoParams struct {
	// CompositionText is the active composition's bytes — the bytes
	// inserted into the rendering text at SelectionStart, replacing
	// committed[SelectionStart:SelectionEnd].
	CompositionText string

	// LineByteOffsets is the logical-line layout of the committed text.
	LineByteOffsets *LineByteOffsets

	// SelectionStart and SelectionEnd are byte offsets into the
	// committed text describing the range the composition replaces.
	// SelectionStart == SelectionEnd for a pure insertion.
	SelectionStart int
	SelectionEnd   int

	// AutoWrap toggles the visual-Y delta measurement for the
	// selection line. When false, RenderingYShift in the result is
	// always 0 and the fields below are ignored.
	AutoWrap bool

	// CommittedSelectionLine and RenderingSelectionLine are the bytes
	// of the logical line containing the selection (SelectionStart ..
	// SelectionEnd, which always lies within a single logical line —
	// the function rejects multi-line selections), in committed and
	// rendering coordinates respectively. Required when AutoWrap is
	// true; ignored otherwise.
	CommittedSelectionLine string
	RenderingSelectionLine string

	// Face, LineHeight, TabWidth, KeepTailingSpace are passed through
	// to [MeasureLogicalLineHeight] when AutoWrap is true.
	Face             text.Face
	LineHeight       float64
	TabWidth         float64
	KeepTailingSpace bool

	// WrapWidth is the pixel width at which logical lines wrap into
	// visual sublines. Values <= 0 are treated as math.MaxInt (no
	// wrapping).
	WrapWidth int
}

CompositionInfoParams describes the inputs for ComputeCompositionInfo.

type DrawOptions

type DrawOptions struct {
	Options

	TextColor color.Color

	DrawSelection  bool
	SelectionStart int
	SelectionEnd   int
	SelectionColor color.Color

	DrawComposition          bool
	CompositionStart         int
	CompositionEnd           int
	CompositionActiveStart   int
	CompositionActiveEnd     int
	InactiveCompositionColor color.Color
	ActiveCompositionColor   color.Color
	CompositionBorderWidth   float32

	// VisibleBounds, when non-empty, restricts per-line drawing to lines that
	// intersect this rectangle. Lines fully above or below are skipped without
	// shaping, which matters for very long text whose [bounds] greatly exceed
	// the on-screen viewport. When empty, [bounds] is used (back-compatible).
	VisibleBounds image.Rectangle
}

type HorizontalAlign

type HorizontalAlign int
const (
	HorizontalAlignStart HorizontalAlign = iota
	HorizontalAlignCenter
	HorizontalAlignEnd
	HorizontalAlignLeft
	HorizontalAlignRight
)

type LineByteOffsets

type LineByteOffsets struct {
	// contains filtered or unexported fields
}

LineByteOffsets holds the byte offsets where each logical line (segment separated by hard line breaks) begins within a source string. It is a precomputed sidecar that enables O(log n) line<->byte-offset lookups without rescanning the text.

After a rebuild the first entry is always 0 and the entries are strictly increasing. A trailing line break in the source string creates an extra empty line at the end (e.g. "abc\n" has two logical lines).

func (*LineByteOffsets) ByteOffsetByLineIndex

func (l *LineByteOffsets) ByteOffsetByLineIndex(i int) int

ByteOffsetByLineIndex returns the byte offset of the start of the i-th logical line. Panics if i is out of range.

func (*LineByteOffsets) LineCount

func (l *LineByteOffsets) LineCount() int

LineCount returns the number of logical lines.

The empty string has one logical line. A trailing line break creates an extra empty line, so "abc\n" has two logical lines.

func (*LineByteOffsets) LineIndexForByteOffset

func (l *LineByteOffsets) LineIndexForByteOffset(byteOffset int) int

LineIndexForByteOffset returns the index of the logical line that contains byteOffset. byteOffset is clamped: negative values map to line 0 and values past the text map to the last line.

func (*LineByteOffsets) Rebuild

func (l *LineByteOffsets) Rebuild(scan func(io.Writer) error) error

Rebuild discards any current contents and rescans the bytes written by scan for logical-line starts. The io.Writer passed to scan accepts bytes in any number of chunks; the recorded offsets are the same as if the concatenated bytes had been scanned in a single pass. Any error from scan is returned unchanged after the trailing partial-break state has been flushed.

func (*LineByteOffsets) Replace

func (l *LineByteOffsets) Replace(newText string, start, end int, startCtx, endCtx string, atEOT bool)

Replace updates the offsets to reflect a splice that replaced the source text's [start, end) byte range with newText. The caller must invoke Replace immediately after applying the same splice to the underlying source.

startCtx is the up-to-2-byte slice of pre-splice text immediately before start (positions [max(0, start-2), start)); it is used to derive the scanner's entry state at start so a partial break ending at the splice boundary disambiguates correctly. endCtx is the up-to-3-byte slice of post-splice text immediately after newText (positions [start+len(newText), start+len(newText)+3)); it provides the lookahead the scanner needs for breaks formed at the splice's far boundary. atEOT must be true iff endCtx reaches end-of-text (i.e. start+len(newText)+3 ≥ post-splice length).

After Replace returns, the offsets describe the post-splice text.

func (*LineByteOffsets) Reset

func (l *LineByteOffsets) Reset()

Reset clears the offsets. After Reset, LineCount returns 0; callers that expect at least one line must rebuild first.

type Options

type Options struct {
	AutoWrap         bool
	Face             text.Face
	LineHeight       float64
	HorizontalAlign  HorizontalAlign
	VerticalAlign    VerticalAlign
	TabWidth         float64
	KeepTailingSpace bool
	EllipsisString   string
}

type TextIndexFromPositionParams

type TextIndexFromPositionParams struct {
	// Position is the (x, y) point in the rendering plane to query.
	// Y is measured from the top of the rendered text.
	Position image.Point

	// RenderingTextRange returns rendering[start:end), where the
	// rendering text is the committed text with any active composition
	// spliced in. RenderingTextLength is the total byte length of the
	// rendering text. Required: all reads of the rendering text — both
	// the fast path and the slow-path fallback — go through this
	// callback so the caller never has to materialize the full
	// document.
	RenderingTextRange  func(start, end int) string
	RenderingTextLength int

	// Width is the rendering width.
	Width int

	// Options carries face, lineHeight, autoWrap, alignment, tab
	// width, etc.
	Options *Options

	// CommittedTextRange returns committed[start:end). Required when
	// CompositionLen > 0; ignored otherwise.
	CommittedTextRange func(start, end int) string

	// LineByteOffsets is the logical-line layout of the committed text.
	// Optional; when nil [TextIndexFromPosition] falls back to an
	// O(documentLen) walk of every visual line.
	LineByteOffsets *LineByteOffsets

	// SelectionStart, SelectionEnd, CompositionLen describe an active
	// IME composition: bytes [SelectionStart, SelectionEnd) in the
	// committed text are replaced with bytes [SelectionStart,
	// SelectionStart+CompositionLen) in the rendering text.
	// CompositionLen == 0 means no active composition; the other
	// fields are ignored in that case.
	SelectionStart int
	SelectionEnd   int
	CompositionLen int

	// LogicalLineIndexHint / VisualLineIndexHint are an optional hint
	// that tells [TextIndexFromPosition] where to start its per-
	// logical-line walk instead of starting from line 0.
	// LogicalLineIndexHint is a logical-line index in committed text;
	// VisualLineIndexHint is the cumulative number of visual lines
	// preceding that logical line in committed text. The walk steps
	// forward (or backward) from the hint measuring one logical line
	// at a time, so a caller that places the hint inside its viewport
	// pays O(visible lines) of typesetting per query instead of
	// walking from the document top.
	//
	// Both fields are optional. The zero value means "start from line
	// 0," equivalent to walking from the top of the document — correct
	// but O(documentLen) when the click is far down. Used only when
	// LineByteOffsets is set.
	LogicalLineIndexHint int
	VisualLineIndexHint  int
}

TextIndexFromPositionParams describes the inputs for TextIndexFromPosition. The first group of fields is always required; the second group is optional state that enables the sidecar-accelerated fast path.

type TextPosition

type TextPosition struct {
	X      float64
	Top    float64
	Bottom float64
}

func TextPositionFromIndex

func TextPositionFromIndex(p *TextPositionFromIndexParams) (position0, position1 TextPosition, count int)

TextPositionFromIndex returns the visual position(s) corresponding to p.Index in the rendering text. When p.LineByteOffsets is supplied, the visual-line walk is localized: it starts from (p.LogicalLineIndexHint, p.VisualLineIndexHint) and steps forward (or backward) one logical line at a time, measuring per-line wrap counts, until it reaches the logical line containing p.Index. With the hint placed inside the viewport the cost is O(visible logical lines) per query, instead of the O(documentLen) full scan the sidecar-less fallback performs.

When an active IME composition splices into the rendering text, the committed-text sidecar is reused: byte/visual-line shifts derived from ComputeCompositionInfo map between committed and rendering coordinates without rebuilding the sidecar. Falls back to the unrestricted whole-document walk when the composition crosses a logical-line boundary, when no sidecar is supplied, or when the document is empty. The fallback is observationally equivalent to the fast path.

func TextPositionFromIndexInLogicalLine

func TextPositionFromIndexInLogicalLine(width int, logicalLine string, index int, options *Options) (position0, position1 TextPosition, count int)

TextPositionFromIndexInLogicalLine returns the visual position(s) within one logical line corresponding to the given byte index inside that line. The Y values are relative to the top of the logical line (so the caller can offset them by the line's origin Y). Counterpart of TextPositionFromIndex.

index is a byte offset in [0, len(logicalLine)]. Out-of-range values yield (TextPosition{}, TextPosition{}, 0).

type TextPositionFromIndexParams

type TextPositionFromIndexParams struct {
	// Index is the byte offset in the rendering text to query.
	Index int

	// RenderingTextRange returns rendering[start:end), where the
	// rendering text is the committed text with any active composition
	// spliced in. RenderingTextLength is the total byte length of the
	// rendering text. Required: all reads of the rendering text — both
	// the fast path and the slow-path fallback — go through this
	// callback so the caller never has to materialize the full
	// document.
	RenderingTextRange  func(start, end int) string
	RenderingTextLength int

	// Width is the rendering width.
	Width int

	// Options carries face, lineHeight, autoWrap, alignment, tab
	// width, etc.
	Options *Options

	// CommittedTextRange returns committed[start:end). Required when
	// CompositionLen > 0; ignored otherwise.
	CommittedTextRange func(start, end int) string

	// LineByteOffsets is the logical-line layout of the committed text.
	// Optional; when nil [TextPositionFromIndex] falls back to an
	// O(documentLen) walk of every visual line.
	LineByteOffsets *LineByteOffsets

	// SelectionStart, SelectionEnd, CompositionLen describe an active
	// IME composition: bytes [SelectionStart, SelectionEnd) in the
	// committed text are replaced with bytes [SelectionStart,
	// SelectionStart+CompositionLen) in the rendering text.
	// CompositionLen == 0 means no active composition; the other
	// fields are ignored in that case.
	SelectionStart int
	SelectionEnd   int
	CompositionLen int

	// LogicalLineIndexHint / VisualLineIndexHint pin the result's Y
	// coordinate system: the function treats the logical line at
	// LogicalLineIndexHint as starting at visual-line index
	// VisualLineIndexHint, and walks forward (or backward) from there
	// to whichever line contains Index. The returned position's Top
	// is therefore measured in the caller's coordinate system —
	// (0, 0) means "Y is measured from line 0," matching the legacy
	// behavior; (firstLogicalLineInViewport, 0) means "Y is measured
	// from the first visible line's top," used by virtualized text.
	//
	// The walk is bounded by the logical-line distance between the
	// hint and the line containing Index, so a caller that pins the
	// hint inside its viewport pays only O(visible) typesetting per
	// query. Used only when LineByteOffsets is set and Options.AutoWrap
	// is true.
	LogicalLineIndexHint int
	VisualLineIndexHint  int
}

TextPositionFromIndexParams describes the inputs for TextPositionFromIndex. The first group of fields is always required; the second group is optional state that enables the sidecar-accelerated fast path.

type VerticalAlign

type VerticalAlign int
const (
	VerticalAlignTop VerticalAlign = iota
	VerticalAlignMiddle
	VerticalAlignBottom
)

type VisibleRange

type VisibleRange struct {
	// FirstLine and LastLine are the inclusive range of logical-line
	// indices the caller should draw.
	FirstLine, LastLine int

	// StartInBytes and EndInBytes are the byte range of the rendering
	// text the caller should draw: rendering[StartInBytes:EndInBytes].
	StartInBytes, EndInBytes int

	// YShift is added to the drawing-origin Y so the first sliced line
	// lands at its original screen Y. Already includes the alignment-
	// specific portion of the original Y offset, so the caller forces
	// [VerticalAlignTop] when calling [Draw].
	YShift int
}

VisibleRange is the result of VisibleRangeInViewport when its ok return is true.

func VisibleRangeInViewport

func VisibleRangeInViewport(p *VisibleRangeInViewportParams) (VisibleRange, bool)

VisibleRangeInViewport returns the byte range and logical-line indices that cover the visible region when the widget is positioned so FirstLogicalLineInViewport sits at widget-local Y=0. The walk steps forward from FirstLogicalLineInViewport, measuring each logical line's wrap count on the fly, so a caller pinned to the topmost visible line pays only O(visible) typesetting per query. Composition splices on lines past the splice are handled by reading rendering-text bytes for the composition's selection line.

ok is false when the document is empty.

VerticalAlign is intentionally not part of the input: when the caller pins the viewport at a non-zero logical line, the document is assumed to overflow the viewport (the case where alignment matters), so YShift is always 0 and the caller's bounds positioning carries any needed offset itself.

type VisibleRangeInViewportParams

type VisibleRangeInViewportParams struct {
	// FirstLogicalLineInViewport is the logical line whose top sits
	// at the widget-local origin (Y=0). The caller's bounds-positioning
	// places this line at the top of the rendered output, so the
	// returned VisibleRange.FirstLine is always this index (clamped to
	// the document) and YShift is always 0.
	FirstLogicalLineInViewport int

	// LineByteOffsets is the logical-line layout of the committed
	// text. The number of logical lines comes from its LineCount.
	LineByteOffsets *LineByteOffsets

	// RenderingTextRange returns rendering[start:end). The walker
	// reads each measured line through this callback so the caller
	// never has to materialize the full rendering text. Required when
	// AutoWrap is true (so the walker can shape per-line content); for
	// AutoWrap=false only RenderingTextLength is consulted.
	RenderingTextRange func(start, end int) string

	// RenderingTextLength is the total byte length of the rendering
	// text.
	RenderingTextLength int

	// ViewportSize describes the rendering box the walker operates
	// against: X is the wrap width passed through to
	// [VisualLineCountForLogicalLine] when AutoWrap is true, and Y is
	// the distance below FirstLogicalLineInViewport's top that the
	// visible region extends downward. The walk stops once cumulative
	// line heights exceed Y, leaving one line of slack so the
	// caller's inner Y clip can handle off-by-one rounding.
	ViewportSize image.Point

	// Face, LineHeight, TabWidth, KeepTailingSpace are passed through
	// to [VisualLineCountForLogicalLine] when AutoWrap is true.
	Face             text.Face
	LineHeight       float64
	TabWidth         float64
	KeepTailingSpace bool

	// AutoWrap toggles between a per-line shaping walk (true) and a
	// flat LineHeight*idx arithmetic (false).
	AutoWrap bool

	// Composition is the splice info from [ComputeCompositionInfo].
	// The zero value means "no active composition".
	Composition CompositionInfo
}

VisibleRangeInViewportParams describes the inputs for VisibleRangeInViewport. The walk steps forward from FirstLogicalLineInViewport measuring per-line heights via VisualLineCountForLogicalLine until cumulative height covers Height, so the cost is O(visible logical lines) — the prefix [0, FirstLogicalLineInViewport) is never measured.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL