engine

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Oct 4, 2026 License: Apache-2.0 Imports: 16 Imported by: 0

Documentation

Overview

Package engine is the pipeline from a document's blocks to pixels: it turns the block and inline tree the compiler builds into layout boxes, and draws and hit-tests them on a canvas.Canvas.

It covers the block and inline types with their layouts, line layout, the rendering context (Context), the lazily laid-out top level (StackBox, which answers position queries but keeps no position of its own), sideways-scrolling blocks (ScrollBox) and diagram blocks. It depends only on contracts (ast, styling, fonts, images, canvas) and the image cache, never on themes, the compiler or the public whynot package.

State is not its job: the scroll position, sideways offsets, hover and scrollbar timing belong to whynot.View, which hands drawing what it needs through Context (the ScrollOffset and Scrollbar hooks). Neither is parsing Markdown, input handling, or the public API.

Index

Constants

View Source
const NaturalWidthMeasure = 1 << 20

NaturalWidthMeasure is an effectively-unbounded width passed to a cell's GetBlockLayout purely to measure its natural (unwrapped) width via the resulting BlockLayout's Bounds() - large enough that no realistic cell content would ever wrap against it.

Variables

This section is empty.

Functions

func DrawBlockLayout

func DrawBlockLayout(box BlockLayout, dst canvas.Canvas, x, y int, now time.Duration)

DrawBlockLayout is the sole entry point for drawing a BlockLayout: it skips drawContents entirely when box's bounds don't overlap dst, so every BlockLayout gets that for free regardless of who's calling it or where it sits in the tree.

Types

type Axis

type Axis int

Axis is the direction a region scrolls in.

const (
	Horizontal Axis = iota
	Vertical
)

type Block

type Block interface {
	Source
	GetBlockLayout(ctx Context, width int) BlockLayout
	Margins(ctx Context) styling.Margins
}

func NewDiagramBlock

func NewDiagramBlock(node *ast.Node, img images.AsyncImage, fallback Block) Block

NewDiagramBlock returns a Block that renders img's image once it resolves, and fallback's own layout - typically the same CodeBlock that would've rendered without a plugin - while it's still pending or failed, so a diagram's raw source stays visible rather than a bare placeholder message. See codeblocks.Plugin.

type BlockLayout

type BlockLayout interface {
	Bounds() image.Rectangle
	Source() Source

	HitTest(p image.Point) (hit Hit, offset image.Point)
	// PendingImages returns the resolved image srcs this box - or
	// anything nested inside it - is still waiting to settle (still
	// fetching, or failed but eligible for retry). Empty once nothing
	// nested is still unsettled - such a box never needs revisiting due
	// to image state, only a genuine structural change (width, scale,
	// StyleSheet), same as any other content. Not memoized: only called
	// from View.invalidateChangedImages, itself only reached when
	// ImageCache reports something actually changed - not a per-frame
	// cost - and a resolved top-level slot's subtree never mutates in
	// place (see StackBox.boxAt), so there's nothing to go stale.
	PendingImages() []string
	// contains filtered or unexported methods
}

BlockLayout is drawn via the package-level DrawBlockLayout, not by calling drawContents directly, so that every BlockLayout gets the same off-screen skip for free regardless of where it sits in the tree. drawContents holds only the type-specific drawing logic.

HitTest identifies what's at p, a point in this BlockLayout's own Bounds() frame - callers must check p is in Bounds() first, since an implementation never re-checks. A nil hit means no match (dead space, e.g. a margin or padding); offset is where hit.Bounds() should be placed to land in the same frame p arrived in - a composite BlockLayout recursing into a child must add its own offset to the child's.

type BlockquoteBlock

type BlockquoteBlock struct {
	Inner   Block
	ASTNode *ast.Node
	// contains filtered or unexported fields
}

BlockquoteBlock is a quoted group of ordinary blocks (`> ...`). Unlike list-item indentation, which relies on the parent StackBlock's generic left-margin wrapping, a blockquote positions its own content and draws its own left-edge bar - self-contained, so nested blockquotes (each level's bar drawn independently) just work without the parent needing to know anything about it. inner is the quoted content as a single Block - already a StackBlock if there was more than one, resolved once at compile time rather than rebuilt on every GetBlockLayout call.

func (*BlockquoteBlock) GetBlockLayout

func (b *BlockquoteBlock) GetBlockLayout(ctx Context, width int) BlockLayout

func (BlockquoteBlock) Margins

func (BlockquoteBlock) Margins(ctx Context) styling.Margins

func (*BlockquoteBlock) Node

func (b *BlockquoteBlock) Node() *ast.Node

type BlockquoteBox

type BlockquoteBox struct {
	Indent int

	Inner BlockLayout
	// contains filtered or unexported fields
}

BlockquoteBox draws a vertical bar down the left edge and positions its inner content (the quote's own blocks, already laid out at the reduced width) to the right of it - the visual marker for a `>` blockquote.

func (*BlockquoteBox) Bounds

func (b *BlockquoteBox) Bounds() image.Rectangle

func (*BlockquoteBox) HitTest

func (b *BlockquoteBox) HitTest(p image.Point) (Hit, image.Point)

HitTest treats the whole indent strip (bar plus any padding before inner starts) as the blockquote's own, and falls back to it too whenever inner declines to match (content narrower than the indented width, or a gap between quoted paragraphs) - every HitTest implementation degrades gracefully on an out-of-bounds point, so there's no need to bounds-check before delegating.

func (*BlockquoteBox) PendingImages

func (b *BlockquoteBox) PendingImages() []string

func (*BlockquoteBox) Source

func (b *BlockquoteBox) Source() Source

type CellAlignment

type CellAlignment int
const (
	AlignNone CellAlignment = iota
	AlignLeft
	AlignRight
	AlignCenter
)

type CodeBlock

type CodeBlock struct {

	// Lines is one slice per visual line, each holding that line's spans -
	// usually a single element (one InlineText for the whole line), but
	// more than one when a code-block plugin has split the line into classified
	// tokens (see highlightLines).
	Lines   [][]Inline
	ASTNode *ast.Node
	// contains filtered or unexported fields
}

func (*CodeBlock) GetBlockLayout

func (b *CodeBlock) GetBlockLayout(ctx Context, width int) BlockLayout

func (CodeBlock) Margins

func (CodeBlock) Margins(ctx Context) styling.Margins

func (*CodeBlock) Node

func (b *CodeBlock) Node() *ast.Node

type Context

type Context struct {
	Scale        float64
	FaceSelector fonts.FaceSelector
	Styles       styling.Styles

	// HighlightNode is the ast.Node currently under the mouse (e.g. a
	// hovered link), or nil - see ResolvedColor. Set by View's hover, which
	// rebuilds the layout tree when it changes.
	HighlightNode *ast.Node

	// ImageCache resolves, fetches, and decodes images, caching the
	// result - see imagecache.Cache. NewView always sets one (images.FileSource
	// unless overridden via WithImageSource). If nil, images aren't
	// loaded at all: an image renders as its alt text, and a diagram as
	// its source code block.
	ImageCache *imagecache.Cache

	// ScrollOffset is how far source's content is scrolled along axis,
	// unclamped: a ScrollBox clamps it to its content. Scroll state is the
	// View's; nil means nothing ever scrolls.
	ScrollOffset func(source Block, axis Axis) float64

	// Scrollbar is called as each ScrollBox is drawn, with where its
	// scrollbar would go; nil means no scrollbars.
	Scrollbar ScrollbarFunc

	// Time is elapsed time since the embedder started rendering (its
	// own reference point - only ever used relative to itself, never
	// compared against a wall-clock timestamp), set by every View.Draw. The one consumer today is imagecache.Animation.CurrentFrame,
	// reached via DrawInline - kept here rather than read from a direct
	// time.Now() call so frame selection stays a pure, deterministic
	// function of its inputs, easy to test without any real waiting.
	Time time.Duration
}

func (Context) ResolvedColor

func (c Context) ResolvedColor(node *ast.Node) color.Color

ResolvedColor walks node's ancestry (node itself first) for the nearest non-nil Color contribution - mirrors CSS's `color`, which inherits down from the nearest ancestor that sets it.

func (Context) ResolvedTextStyle

func (c Context) ResolvedTextStyle(node *ast.Node) fonts.TextStyle

ResolvedTextStyle merges node's ancestry's TextStyle contributions into one TextStyle - the nearest ancestor (node itself first) to set a given field wins, so e.g. Strong nested inside Emphasis picks up both a bold weight (from Strong) and an italic style (from Emphasis). Walks one step past the root (node == nil) so StyleSheet's baseline contribution can fill in any field nothing along the way ever claimed.

func (Context) ScaledMargins

func (c Context) ScaledMargins(m marginer) styling.Margins

func (Context) ScaledViewMargins

func (c Context) ScaledViewMargins() styling.Margins

type DiagramBlock

type DiagramBlock struct {
	Image    images.AsyncImage
	Fallback Block
	// contains filtered or unexported fields
}

func (*DiagramBlock) GetBlockLayout

func (b *DiagramBlock) GetBlockLayout(ctx Context, width int) BlockLayout

func (DiagramBlock) Margins

func (DiagramBlock) Margins(ctx Context) styling.Margins

func (*DiagramBlock) Node

func (b *DiagramBlock) Node() *ast.Node

type EmptyBox

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

func NewEmptyBox

func NewEmptyBox(w, h int) *EmptyBox

func (*EmptyBox) Bounds

func (b *EmptyBox) Bounds() image.Rectangle

func (*EmptyBox) HitTest

func (b *EmptyBox) HitTest(p image.Point) (Hit, image.Point)

HitTest always misses: an EmptyBox is pure spacing (a margin/gap between blocks or lines), never content.

func (*EmptyBox) PendingImages

func (b *EmptyBox) PendingImages() []string

func (*EmptyBox) Source

func (b *EmptyBox) Source() Source

type Hit

type Hit interface {
	Bounds() image.Rectangle
	Source() Source
}

Hit is what HitTest returns for an actual match: something that can report its own Bounds() and Source() - every BlockLayout and InlineLayout qualifies, so HitTest can return either kind of leaf uniformly. A composite's Source() is nil when it aggregates children with no single identity of their own (see StackBox.Source), the same way StackBlock.Node does for the semantic tree.

type ImageBox

type ImageBox struct {
	// Image is the already-decoded image imagecache.Cache.Load returned -
	// carried forward from InlineImage.GetInlineLayout so DrawInline
	// can pass it straight to Canvas.DrawImage, which never fetches or
	// decodes anything itself. nil while the image is still pending but
	// its dimensions are already known - bounds is still the correct,
	// final (scaled) size, so DrawInline draws a placeholder rect
	// instead, and nothing needs to reflow once Image is filled in later.
	// anim is set instead of Image for an animated GIF - exactly one of
	// the two is non-nil on a settled ImageBox.
	Image     image.Image
	Animation *imagecache.Animation
	Rect      image.Rectangle

	// Pending is the one resolved src this box is still waiting on,
	// while img == nil - see PendingImages.
	Pending []string
	// contains filtered or unexported fields
}

func (*ImageBox) Bounds

func (b *ImageBox) Bounds() image.Rectangle

func (*ImageBox) BoundsAndAdvance

func (b *ImageBox) BoundsAndAdvance() (image.Rectangle, int)

func (*ImageBox) DrawInline

func (b *ImageBox) DrawInline(dst canvas.Canvas, x, y int, now time.Duration)

func (*ImageBox) Glued

func (b *ImageBox) Glued() bool

func (*ImageBox) HitTest

func (b *ImageBox) HitTest(p image.Point, x, y int) (Hit, image.Point)

func (*ImageBox) PendingImages

func (b *ImageBox) PendingImages() []string

func (*ImageBox) Source

func (b *ImageBox) Source() Source

func (*ImageBox) SpaceWidth

func (b *ImageBox) SpaceWidth() int

type Inline

type Inline interface {
	Source
	// GetInlineLayout builds this Inline's layout, width the same
	// effectively-unbounded-or-real wrap width a Block's own
	// GetBlockLayout receives (see naturalWidthMeasure) - only
	// InlineImage uses it (see fitWidth), every other implementation
	// ignores it.
	GetInlineLayout(ctx Context, width int) InlineLayout
}

type InlineImage

type InlineImage struct {
	Src     string
	Alt     string
	Title   string
	ASTNode *ast.Node
	// FallbackNode is a ast.TagUnsupported child of node - see the
	// compiler's KindImage case (internal/markdown/inlines.go) for why
	// it's precomputed once, at parse time, rather than created on demand
	// here.
	FallbackNode *ast.Node

	// Glued - see InlineLayout.Glued's doc comment and InlineText.Glued.
	Glued bool
}

func (*InlineImage) GetInlineLayout

func (i *InlineImage) GetInlineLayout(ctx Context, width int) InlineLayout

GetInlineLayout resolves, fetches, and decodes src via ctx.ImageCache (letting the embedder decide the resolution/fetch policy - relative to a document's location, over http(s), from an archive, whatever it needs) - never blocking, since imagecache.Cache.Load never does. With no image cache, the image isn't loaded and its fallback text is shown. Otherwise there are three outcomes:

  • Ready: the decoded image, scaled by ctx.Scale like every other sized quantity in the layout system (Context.ScaledMargins and friends), so images grow and shrink along with zoom/DPI instead of staying pixel-locked - then capped to width, preserving aspect ratio, if that scaled size would still be wider (see fitWidth) - the inline layer's equivalent of CSS's max-width: 100%, so an image dropped into a document at its own native resolution doesn't overflow the page.
  • Pending with bounds already known (the header-peek in imagecache.Cache.fetchAndDecode beat the caller here): an ImageBox at the correct, final (scaled and width-capped) size, but with no image yet - DrawInline draws a placeholder instead, and nothing needs to reflow once the real pixels arrive later.
  • Pending with no bounds yet, or Failed (unreadable, undecodable, or mid-retry-cooldown): falls back to text instead of a silent zero-size gap - reusing InlineText's own GetInlineLayout, so it renders exactly like any other construct whynot can't handle (see appendUnsupportedInline). Either way the resulting TextBox is stamped with which src it's standing in for, so View.invalidateChangedImages knows to revisit it once that changes.

func (*InlineImage) Node

func (i *InlineImage) Node() *ast.Node

type InlineLayout

type InlineLayout interface {
	// BoundsAndAdvance returns the part's ink bounds relative to its pen
	// position, and how far it moves the pen.
	BoundsAndAdvance() (image.Rectangle, int)
	Bounds() image.Rectangle
	Source() Source
	SpaceWidth() int
	// Glued reports whether this item directly abuts the previous one in
	// its line with no source whitespace between them - e.g. a word right
	// next to a code span, or a non-breaking space's own neighbors. A
	// glued boundary gets no gap and is never a line-break point (see
	// lineBuilder.gap and wrapLines). Meaningless for an item that's
	// never anything but a line's first part (listItemMarkerBox,
	// checkboxBox) - never queried there.
	Glued() bool
	// DrawInline's now - see BlockLayout.drawContents's identical parameter.
	DrawInline(dst canvas.Canvas, x, y int, now time.Duration)
	HitTest(p image.Point, x, y int) (hit Hit, offset image.Point)
	// PendingImages - see BlockLayout's identical method.
	PendingImages() []string
}

InlineLayout is one part of a line of inline content, placed by lineBuilder. DrawInline and HitTest take the pen position LineBox placed it at: x the start of its advance, y the baseline.

type InlineText

type InlineText struct {
	Text    string
	ASTNode *ast.Node

	// Glued - see InlineLayout.Glued's doc comment. Set by the compiler
	// (compiler.appendString/pendingSpace) from whether the
	// source actually had whitespace immediately before this item;
	// false (the zero value) for every InlineText built directly rather
	// than through the compiler, matching the old always-space behavior.
	Glued bool
}

func (*InlineText) GetInlineLayout

func (t *InlineText) GetInlineLayout(ctx Context, width int) InlineLayout

func (*InlineText) Node

func (t *InlineText) Node() *ast.Node

type LineBox

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

LineBox is one line of inline content, each part at a precomputed x. Parts are placed once, by lineBuilder, and drawing, hit-testing and bounds all read those positions, so what's measured is what's drawn.

func NewLineBox

func NewLineBox(parts []InlineLayout, glue bool) *LineBox

NewLineBox places parts on a single line, never wrapping. glue abuts parts with no gap between them - for parts that are already contiguous slices of one string, whitespace included (CodeBlock's token spans).

func (*LineBox) Bounds

func (b *LineBox) Bounds() image.Rectangle

Bounds only compensates for a negative bounds.Min.X, not a positive one: a code line's leading indentation has advance but no ink, so it's occupied space starting at the line's x=0, not excess to crop away.

func (*LineBox) HitTest

func (b *LineBox) HitTest(p image.Point) (Hit, image.Point)

HitTest checks each part where drawContents draws it for external y=0, which lands a part's footprint in Bounds()'s frame - the frame p is in.

func (*LineBox) PendingImages

func (b *LineBox) PendingImages() []string

func (*LineBox) Source

func (b *LineBox) Source() Source

Source is always nil: a line can mix parts with different identities (e.g. plain text next to emphasized text); HitTest returns the specific part that matched instead.

type ListItemHeadBlock

type ListItemHeadBlock struct {
	Marker  Inline
	Parts   []Inline
	ASTNode *ast.Node
	// contains filtered or unexported fields
}

ListItemHeadBlock is a list item's own paragraph text, flowed with the marker hanging off the first line - see GetBlockLayout. It always reports zero margins: a list item's indentation and item-to-item spacing belong to the StackBlock compileListItem wraps it in (along with any trailing content, e.g. a nested list), not to the head on its own - it has no business claiming indentation whether or not there's a trailing part.

func (*ListItemHeadBlock) GetBlockLayout

func (b *ListItemHeadBlock) GetBlockLayout(ctx Context, width int) BlockLayout

func (ListItemHeadBlock) Margins

func (ListItemHeadBlock) Margins(ctx Context) styling.Margins

func (*ListItemHeadBlock) Node

func (b *ListItemHeadBlock) Node() *ast.Node

type MarginBlock

type MarginBlock struct {
	Block
	MarginNode *ast.Node
}

MarginBlock adds margins to an existing Block, collapsing Top/Bottom with whatever it already reports. Left/Right are just MarginBlock's own value: nothing it wraps ever reports a nonzero Left/Right to collapse with - StackBlock, the only Block with any derived margins, only ever derives Top/Bottom from its children.

Its own margins are resolved from ctx.Styles via node - unscaled, matching Block.Margins' convention (StackBlock.GetBlockLayout, the one real caller, scales the result via ctx.ScaledMargins).

func (*MarginBlock) Margins

func (b *MarginBlock) Margins(ctx Context) styling.Margins

func (*MarginBlock) Node

func (b *MarginBlock) Node() *ast.Node

Node delegates to the wrapped Block rather than returning b.node: a loose list item's head wraps a ListItemHeadBlock in a MarginBlock keyed to a synthetic ast.TagParagraph node (purely so its margins resolve like a paragraph's) - the wrapped content's ast.TagListItem is the more correct identity for hit-testing.

type RuleBox

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

RuleBox is a single filled horizontal bar - the box for a thematic break (`---`). Its own height is just the bar's thickness; the visual spacing above and below comes from ThematicBreakBlock's Margins, same as any other block.

func (*RuleBox) Bounds

func (b *RuleBox) Bounds() image.Rectangle

func (*RuleBox) HitTest

func (b *RuleBox) HitTest(p image.Point) (Hit, image.Point)

func (*RuleBox) PendingImages

func (b *RuleBox) PendingImages() []string

func (*RuleBox) Source

func (b *RuleBox) Source() Source

type ScrollBox

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

ScrollBox shows a window, width wide, onto content that's wider, scrolled horizontally by the offset its View keeps for source. An edge fades into the page background wherever there's hidden content, and a scrollbar is drawn by the Context's Scrollbar hook, if any.

func (*ScrollBox) Bounds

func (b *ScrollBox) Bounds() image.Rectangle

func (*ScrollBox) HitTest

func (b *ScrollBox) HitTest(p image.Point) (Hit, image.Point)

func (*ScrollBox) PendingImages

func (b *ScrollBox) PendingImages() []string

func (*ScrollBox) Source

func (b *ScrollBox) Source() Source

type ScrollRegion

type ScrollRegion struct {
	Source      Block
	Axis        Axis
	Box         image.Rectangle // the whole region, in its Canvas's coordinates
	Visible     image.Rectangle // Box clipped to what was actually drawn
	ContentSize int             // the content's size along Axis
	Offset      int             // how far the content is scrolled, as drawn
}

ScrollRegion is a part of the document whose content is bigger than what it shows, as drawn this frame: where a scrollbar for it would go.

func (ScrollRegion) MaxOffset

func (r ScrollRegion) MaxOffset() int

MaxOffset is how far r's content can scroll.

type ScrollbarFunc

type ScrollbarFunc func(dst canvas.Canvas, r ScrollRegion, now time.Duration)

ScrollbarFunc draws, or not, the scrollbar of a region as it's drawn onto dst at now. Whoever keeps scroll state provides it (the View), and decides how a scrollbar looks and behaves; it can also note the region, to match input against it later.

type Source

type Source interface {
	Node() *ast.Node
}

Source is the common ground between Block and Inline: something with a semantic identity in the compiled ast.Node tree. Leaf boxes expose it so a hit-test result traces back to its origin - and since the concrete value is the real Block/Inline, a caller can type-assert further for anything beyond the node itself.

type StackBlock

type StackBlock struct {
	Blocks []Block
}

func (*StackBlock) GetBlockLayout

func (b *StackBlock) GetBlockLayout(ctx Context, width int) BlockLayout

GetBlockLayout builds the slot skeleton only - gap sizes from Margins(), which is cheap (no text measurement) - deferring each block's own GetBlockLayout call to StackBox.boxAt, on first access to that slot. A resize only needs to re-derive gap sizes and widths up front; the expensive part (actually laying out each block's content) only happens for slots something later asks for, e.g. those near a scroll anchor.

func (*StackBlock) Margins

func (b *StackBlock) Margins(ctx Context) styling.Margins

Margins reports Top/Bottom as its first/last child's own margin - the same collapsing GetBlockLayout applies between siblings, extended to its own edges. Left/Right are zero: as a stack of blocks arranged vertically, StackBlock has no notion of a horizontal edge to derive from a child - only whatever wraps it (see MarginBlock) has a real Left/Right.

func (*StackBlock) Node

func (b *StackBlock) Node() *ast.Node

Node always returns nil: a StackBlock aggregates other Blocks, each with their own identity, so it has none of its own - hit-testing that reaches a bare StackBlock should already have recursed into whichever child slot actually matched.

func (*StackBlock) StackLayout

func (b *StackBlock) StackLayout(ctx *Context, width int) *StackBox

StackLayout is GetBlockLayout with its concrete result type, laying out slots against *ctx when they're first needed (see StackBox.ctx).

type StackBox

type StackBox struct {
	Slots []StackSlot

	// Context is what unresolved slots are laid out with, when first needed.
	// For a View's top-level StackBox this is the View's own live context,
	// so a change that only affects slots laid out from then on (e.g. the
	// hovered link) needs no rebuild, just invalidating the affected slots.
	Context *Context
	Width   int
	// contains filtered or unexported fields
}

func (*StackBox) AddSpacers

func (b *StackBox) AddSpacers(top, bottom int)

AddSpacers surrounds b's slots with empty slots of the given heights, omitting a zero-height one.

func (*StackBox) Bounds

func (b *StackBox) Bounds() image.Rectangle

func (*StackBox) BoxAt

func (b *StackBox) BoxAt(i int) BlockLayout

BoxAt returns the child at index i, building it from its Block and memoizing the result on first access if it isn't already resolved. A caller that only ever asks for slots near a scroll cursor only ever pays to build those.

func (*StackBox) DrawFrom

func (b *StackBox) DrawFrom(dst canvas.Canvas, c StackCursor, x, y int, now time.Duration)

DrawFrom draws starting at c, so that c's position lands at (x, y) on dst - unlike DrawBlockLayout, it never calls Bounds() on the whole tree first, and never resolves or draws slots before c.index. Intended for View to call at the scroll cursor.

func (*StackBox) HitTest

func (b *StackBox) HitTest(p image.Point) (Hit, image.Point)

HitTest walks slots from the start looking for the one p.Y falls in - a plain linear scan, unlike normalizeCursor's cursor-relative walk, since resolving this way would force-build every slot up to the match regardless. That's fine for a StackBox reached via nested structure (a list's items, a blockquote's paragraphs - typically small), but would defeat the point for the top-level, potentially huge document StackBox - View.HitTest handles that one specially, starting from the current scroll cursor instead of slot 0.

A miss on the matched slot (outside its bounds, or its own HitTest declines - e.g. clicking past the end of a paragraph's short last line) falls back to this StackBox itself when it has a Source - the same fallback-to-self BlockquoteBox/tableBox already do. When it doesn't (an aggregate of unrelated blocks - see Source), it's a genuine miss instead, same as before Source existed.

func (*StackBox) Invalidate

func (b *StackBox) Invalidate(i int)

Invalidate discards slot i's layout; boxAt rebuilds it when next needed.

func (*StackBox) MoveCursor

func (b *StackBox) MoveCursor(c StackCursor, dy float64) StackCursor

MoveCursor returns c shifted by dy (in the offset's own sign convention: positive moves forward through the document) and normalized, for callers that have a cursor already and want to move it rather than construct a new one from scratch - e.g. View.ScrollBy. Layout's resize re-anchoring isn't a move like this (offset is recomputed from a ratio through a slot's new height, not shifted from its old value), so it calls normalizeCursor directly instead.

func (*StackBox) NormalizeCursor

func (b *StackBox) NormalizeCursor(c StackCursor) StackCursor

NormalizeCursor adjusts c so that 0 <= offset < boxAt(index)'s height, walking to neighboring slots as needed rather than scanning from the start: only the slots actually walked over are touched (via boxAt), so cost is proportional to how far a position moved, not to how far into the document it already was.

Past the very end of the document, NormalizeCursor clamps to (lastIndex, height(lastIndex)) - offset equal to the height, not less than it - rather than reporting an ever-growing out-of-range offset. NormalizeCursor is idempotent everywhere except exactly that clamped value.

func (*StackBox) PendingImages

func (b *StackBox) PendingImages() []string

PendingImages aggregates only over already-resolved slots (slot.box != nil) - an unresolved slot hasn't been looked at yet, so there's nothing pending to report about it until something actually asks for it (see boxAt). Not memoized (see the BlockLayout interface's own doc comment) - in particular, the top-level document StackBox's own slots keep resolving lazily over time, so caching this would go stale; View.invalidateChangedImages only ever calls this on individual already-resolved slots' boxes (via documentStack), never on that top-level aggregate itself.

func (*StackBox) Source

func (b *StackBox) Source() Source

type StackCursor

type StackCursor struct {
	Index  int
	Offset float64
}

StackCursor is a position within a StackBox: which slot, and how far into it. Like a text cursor, it's only meaningful relative to the specific StackBox it was resolved against - the same (index, offset) pair means something different for a different StackBox. offset is float64 so scroll deltas can accumulate sub-pixel amounts directly; DrawFrom truncates to a pixel only right before it becomes a screen coordinate.

type StackSlot

type StackSlot struct {
	Box   BlockLayout
	Block Block

	Width int
	// contains filtered or unexported fields
}

StackSlot is a StackBox child that's either already resolved (box set - true of gap spacers, which are cheap enough to build eagerly) or needs building from a Block on first access (block set). A content slot's width is already margin-reduced if wrap is set.

type TableBlock

type TableBlock struct {
	Header  []TableCell
	Rows    [][]TableCell
	ASTNode *ast.Node
	// contains filtered or unexported fields
}

TableBlock is a GFM table. header and each row in rows hold one tableCell per column.

func (*TableBlock) GetBlockLayout

func (b *TableBlock) GetBlockLayout(ctx Context, width int) BlockLayout

func (TableBlock) Margins

func (TableBlock) Margins(ctx Context) styling.Margins

func (*TableBlock) Node

func (b *TableBlock) Node() *ast.Node

type TableCell

type TableCell struct {
	Content   *TextBlock
	Alignment CellAlignment
}

type TaskCheckbox

type TaskCheckbox struct {
	Checked bool
	ASTNode *ast.Node
}

TaskCheckbox is a GFM task list item's checkbox marker (- [ ]/- [x]). Resolved at layout time, not compile time: whether to draw the real ballot-box glyph or a procedurally-drawn box (see checkboxBox) depends on whether the active FaceSelector/StyleSheet's resolved font actually has that glyph, which can change across a re-layout (zoom, theme, a caller registering a different font) even though the semantic tree itself never does - most fonts, including the bundled Go fonts, don't have it.

func (*TaskCheckbox) GetInlineLayout

func (c *TaskCheckbox) GetInlineLayout(ctx Context, width int) InlineLayout

func (*TaskCheckbox) Node

func (c *TaskCheckbox) Node() *ast.Node

type TextBlock

type TextBlock struct {
	Parts   []Inline
	ASTNode *ast.Node
	// contains filtered or unexported fields
}

func (*TextBlock) GetBlockLayout

func (b *TextBlock) GetBlockLayout(ctx Context, width int) BlockLayout

func (TextBlock) Margins

func (TextBlock) Margins(ctx Context) styling.Margins

func (*TextBlock) Node

func (b *TextBlock) Node() *ast.Node

type TextBox

type TextBox struct {
	Text  string
	Face  font.Face
	Color color.Color

	// StrikeThickness is the strikethrough line's thickness in pixels; 0
	// means no strikethrough.
	StrikeThickness int

	// LineHeight is the multiplier applied to Face's natural Ascent+Descent
	// to get this box's vertical extent (StyleSheet.LineHeight) - the
	// font's own metrics don't reliably encode comfortable line spacing
	// (see BoundsAndAdvance), so this is what actually separates
	// consecutive wrapped lines within a paragraph. The zero value (an
	// unset TextBox, e.g. built directly by a test rather than through
	// InlineText.GetInlineLayout) falls back to the pre-LineHeight
	// behavior - bare Ascent+Descent, no extra space.
	LineHeight float64
	// contains filtered or unexported fields
}

func (*TextBox) Bounds

func (b *TextBox) Bounds() image.Rectangle

func (*TextBox) BoundsAndAdvance

func (b *TextBox) BoundsAndAdvance() (image.Rectangle, int)

Text/Face never change after construction, and a TextBox is always rebuilt from scratch (never mutated) whenever the source Block tree is re-laid out, so this measurement is valid for the entire lifetime of the instance: compute it once, on first use.

func (*TextBox) DrawInline

func (b *TextBox) DrawInline(dst canvas.Canvas, x, y int, now time.Duration)

func (*TextBox) Glued

func (b *TextBox) Glued() bool

func (*TextBox) HitTest

func (b *TextBox) HitTest(p image.Point, x, y int) (Hit, image.Point)

func (*TextBox) PendingImages

func (b *TextBox) PendingImages() []string

func (*TextBox) Source

func (b *TextBox) Source() Source

func (*TextBox) SpaceWidth

func (b *TextBox) SpaceWidth() int

type ThematicBreakBlock

type ThematicBreakBlock struct {
	ASTNode *ast.Node
	// contains filtered or unexported fields
}

ThematicBreakBlock is a horizontal rule (`---`). Unlike the other Block types it has no inline content to lay out - just a color; its vertical spacing comes from whatever MarginBlock wraps it.

func (*ThematicBreakBlock) GetBlockLayout

func (b *ThematicBreakBlock) GetBlockLayout(ctx Context, width int) BlockLayout

func (ThematicBreakBlock) Margins

func (ThematicBreakBlock) Margins(ctx Context) styling.Margins

func (*ThematicBreakBlock) Node

func (b *ThematicBreakBlock) Node() *ast.Node

Jump to

Keyboard shortcuts

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