grid

package
v0.16.0 Latest Latest
Warning

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

Go to latest
Published: Aug 25, 2026 License: Apache-2.0 Imports: 11 Imported by: 0

Documentation

Overview

Package grid is the cell grid the whole terminal UI is drawn into: styled grapheme cells, a clipped drawing view over them, and the two ways a frame of them reaches a terminal.

Screen takes the terminal's whole screen and emits the smallest escape stream that turns one frame into the next. Inline draws a block in the terminal's own screen instead, printing finished output above it into the scrollback. They share the cells, the view and the encoding, and differ only in what a frame is allowed to assume about where it is.

It is the only layer that knows what a terminal is made of. Everything above it draws through View and never assembles an escape sequence.

Geometry is image.Rectangle and image.Point from the standard library rather than a private rectangle type. Terminal rectangles are ordinary half-open rectangles, and intersection, insetting and containment are already written and already correct there.

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func ClusterWidth

func ClusterWidth(cluster string) int

ClusterWidth returns Oolong's deterministic estimate of how many terminal columns one grapheme cluster occupies. A control character measures zero.

Everything that lays text out shares this function. Measuring text one way and drawing it another is the cause of every misaligned terminal UI, so there is one answer and one place it comes from.

Display width is terminal behavior, not a complete Unicode property. Different terminals may shape an unusual cluster differently. This estimate fixes ambiguous-width characters to one column, follows go-runewidth's grapheme rules, and separately counts U+FF9E and U+FF9F because common terminals render those halfwidth-katakana sound marks as spacing characters. Other spacing combining marks retain the dependency's answer until terminal evidence supports a rule that does not also break emoji and other ligated clusters.

func EncodeRow

func EncodeRow(cells []Cell, depth Depth) string

EncodeRow renders one row of cells as inline terminal text: style and hyperlink transitions and printable graphemes, and nothing that moves the cursor or erases anything.

It is how a finished transcript line is printed into the terminal's own scrollback, where the line must survive on its own with no screen to address. The result always closes an open hyperlink and returns to the default style, so rows can be concatenated safely. If cells ends inside a multi-column display atom, EncodeRow emits styled blanks for the visible columns rather than a partial atom that would advance the terminal beyond the slice.

Example

EncodeRow turns one finished row into inline terminal text: styles and hyperlinks, and nothing that moves the cursor or erases anything. It is how a transcript line is printed into the terminal's own scrollback, where it has to survive on its own.

package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/grid"
)

func main() {
	s := grid.NewSurface(12, 1)
	v := s.View()
	n := v.Text(0, 0, "see ", grid.Style{})
	v.Text(n, 0, "docs", grid.Style{Attr: grid.Underline})
	v.Link(n, 0, 4, "https://example.test")

	fmt.Printf("%q\n", grid.EncodeRow(s.Row(0), grid.TrueColor))
}
Output:
"see \x1b[0;4m\x1b]8;;https://example.test\x1b\\docs\x1b]8;;\x1b\\\x1b[0m"

func Rect

func Rect(x, y, w, h int) image.Rectangle

Rect builds a rectangle from a terminal-natural origin and size. The result is half-open: it covers columns [x, x+w) and rows [y, y+h). Negative sizes become zero and endpoints that exceed int range saturate.

func Render added in v0.0.3

func Render(w, h int, draw func(View)) []string

Render draws something at a size and returns what it came to, one string per row, with the styling dropped and trailing blanks cut.

It is the way out of the grid for a program that has no terminal: output being piped, a run under a build server, a transcript written to a file. Everything above this package draws into a View and cannot be asked for text any other way, so without this every caller writes the same walk over the cells — which is exactly what every test in this repository had done.

The styling is dropped rather than encoded because that is what "as text" means. A caller that wants the colours as well already has EncodeRow, which is what a frame is made of.

Example

Render is the way out of the grid for a caller with no terminal — a test, a run under a build server, output being piped to a file. Everything above this package draws into a View, so this is where drawing becomes something you can read.

package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/grid"
)

func main() {
	rows := grid.Render(20, 3, func(v grid.View) {
		v.Text(0, 0, "oolong", grid.Style{Attr: grid.Bold})
		v.Text(0, 1, "a terminal library", grid.Style{})
	})
	for _, row := range rows {
		fmt.Printf("%q\n", row)
	}
}
Output:
"oolong"
"a terminal library"
""

Types

type Attr

type Attr uint8

Attr is a set of text attributes.

const (
	Bold Attr = 1 << iota
	Dim
	Italic
	Underline
	Reverse
	Strike
)

The attributes a cell can carry. They are the ones every terminal implements and the ones a single SGR parameter turns on, which is why there are six.

func (Attr) Has

func (a Attr) Has(want Attr) bool

Has reports whether every attribute in want is set.

type Cell

type Cell struct {
	Style Style
	// Link is an OSC 8 hyperlink target. It is cell metadata rather than part of
	// Style because a hyperlink has its own open/close protocol on the wire,
	// while everything in Style is one SGR parameter list.
	Link string
	// contains filtered or unexported fields
}

Cell is one terminal cell.

The zero Cell is a blank single-width cell in the terminal's own style, so a freshly allocated or cleared surface is already valid.

A cell's content is read through Cell.Content rather than a writable field. Only drawing through a View can create content, which keeps its measured span and continuation cells inseparable. Style and Link remain writable on copied rows because changing appearance cannot invalidate that geometry.

func (Cell) Blank

func (c Cell) Blank() bool

Blank reports whether the cell would print as empty space.

func (Cell) Content

func (c Cell) Content() string

Content returns the complete grapheme cluster stored on an atom's head. It is empty for a blank or continuation cell.

func (Cell) Width

func (c Cell) Width() int

Width is how many columns the cell occupies: the complete display width on an atom's head, zero on a continuation cell, and one otherwise.

type Color

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

Color is a cell colour: either the terminal's own default, or a truecolor value. The zero Color is the default, which is what an unstyled cell wants.

func RGBColor

func RGBColor(r, g, b uint8) Color

RGBColor returns a colour that overrides the terminal default.

func (Color) Blend

func (c Color) Blend(over Color, opacity float64) Color

Blend mixes c toward over by opacity, clamped to [0,1].

A colour that defers to the terminal is not a number, so a blend involving one cannot be computed and c is returned unchanged. That is the rule everywhere blending appears: what cannot be resolved is left alone, rather than guessed at. Guessing would tint an interface differently on every terminal, and be wrong in the direction that makes text vanish — a scrim assumed to be over black, painted over white, blacks out the screen.

Turning a default into a number is Ground's job, and doing it first is what makes a blend answerable. A frame drawn by a program has one, because the terminal was asked at startup.

func (Color) Default

func (c Color) Default() bool

Default reports whether the colour defers to the terminal.

func (Color) RGB

func (c Color) RGB() RGB

RGB returns the colour's components. They are meaningless when the colour is the terminal default.

type Cursor

type Cursor struct {
	Visible bool
	Pos     image.Point
	Style   CursorStyle
}

Cursor is where and how the terminal's own cursor should end a frame.

type CursorShape added in v0.5.0

type CursorShape uint8

CursorShape is the terminal cursor's geometry.

const (
	// CursorDefault asks the terminal for its configured default shape. Blink is
	// ignored. It is the zero value and the shape restored when a session ends.
	CursorDefault CursorShape = iota
	// CursorBlock fills one cell.
	CursorBlock
	// CursorUnderline is a line along the bottom of one cell.
	CursorUnderline
	// CursorBar is a vertical line inside one cell.
	CursorBar
)

type CursorStyle added in v0.5.0

type CursorStyle struct {
	Shape CursorShape
	Blink bool
}

CursorStyle is how the terminal's own cursor is drawn. The zero value uses the terminal's default. Blink applies to Block, Underline and Bar.

type Depth

type Depth uint8

Depth is how much colour a terminal is being asked to show.

A frame is always built in truecolor — a Color is either the terminal's default or a 24-bit value, and nothing above this package thinks about anything else. The depth is applied at the very last step, where a style becomes bytes, so a drawing caller never has to know what it is drawing onto and a palette never has to be authored twice.

The zero value is Auto, which leaves the choice to whoever opened the terminal — this package cannot read an environment variable and has no business guessing. Everything here treats it as TrueColor, which is the bet the library made before this type existed; the difference is that it is now a bet a caller can lose gracefully instead of one they cannot opt out of.

const (
	// Auto is the zero value: whatever the caller decides, and truecolor to
	// anything that has to draw before they have.
	Auto Depth = iota
	// TrueColor emits the 24-bit value unchanged.
	TrueColor
	// Depth256 maps each colour to the nearest entry of the xterm 256 palette.
	Depth256
	// Depth16 maps each colour to the nearest of the eight ANSI colours and their
	// bright forms — the only colours a terminal is really obliged to have.
	Depth16
	// NoColor drops colour entirely and keeps the attributes. It is what NO_COLOR
	// asks for, and what a terminal being logged to a file wants: bold and
	// underline still carry meaning in a transcript, and a colour does not.
	NoColor
)

type Drawable added in v0.8.0

type Drawable interface {
	Draw(view View)
	layout.Measurer
}

Drawable is passive content that can measure its height at a width and draw into exactly that space. It is the common contract shared by layout, retained content, and permanent inline publication; layers may add lifecycle meaning but must not invent another drawing shape.

type Ground added in v0.0.2

type Ground struct{ FG, BG Color }

Ground is what a terminal's own two colours actually are.

Leaving a cell's colour at the default is the right way to store it: the user's own theme shows through, an unstyled cell costs nothing on the wire, and a terminal recoloured while a program is running follows along. The price is that "the terminal's own" is not a value, and anything that has to mix with what is underneath needs one. This is where the answer is kept, once the terminal has been asked through the terminal colour-query protocol.

The zero value is two defaults, which is what a terminal that was not asked or did not answer leaves behind. Blending through it resolves nothing and changes nothing, which is the honest outcome: a scrim over an unknown background is a question with no answer, and the visible cost of skipping it — a layer that does not dim what it covers — is far smaller than the cost of guessing.

func (Ground) Resolve added in v0.0.2

func (g Ground) Resolve(s Style) Style

Resolve fills in whatever a style left to the terminal, so a caller that needs numbers has them. What the terminal never said stays default.

Reverse is deliberately not applied. It swaps the two colours on the way to the screen, and swapping them here would mean a caller that resolved a style and drew it back would reverse it twice.

type Inline

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

Inline draws an interface as a block in the terminal's own screen, with output that is finished printed above it.

It is the other way to put frames on a terminal, and the one that makes a program part of a session rather than a mode of it: what the interface has already said stays in the terminal's own scrollback, where the user can scroll back to it, select it, and see it still there after the program exits. A Screen takes a screen of its own and gives back a blank terminal; this keeps the transcript.

Why nothing here is addressed absolutely

The block's position on the terminal is decided by whatever is above it, which this type does not own and cannot ask about. So every frame is written relative to where the last one left the cursor: back to the top of the block, down through its rows, and back to wherever the cursor belongs. Printing works the same way — the rows are written where the block's first row was, and the block is drawn below them, which is what pushes finished output up and into the scrollback.

The block is as tall as what was drawn: the rows up to the last one with anything on it, and never fewer than enough to hold the cursor. Nothing has to declare a height, and an interface that draws two rows occupies two rows.

What a resize costs

A resize is the one thing this cannot get exactly right. The terminal may reflow what is above the block, and there is no way to ask where the block ended up, so the next frame repaints in full from where the cursor was left. That is exact when the terminal did not reflow and approximate when it did, which is the same bargain every inline interface makes.

An Inline must not be copied after first use. Its pending transcript, paired surfaces and terminal cursor model are one publication owner.

func NewInline

func NewInline(w, h int) *Inline

NewInline returns an inline block that may grow to h rows of w columns, whose first flush draws everything.

The height is a ceiling rather than a size: it is what the terminal can spare, and the block takes as much of it as the interface draws into.

func (*Inline) Append added in v0.0.2

func (i *Inline) Append(draw func(View))

Append publishes cells onto the end of the row the last one left open, and leaves it open for the next.

It is what output that does not arrive on line boundaries needs. A reply streaming in three words at a time is one paragraph and not three rows, and a caller with no way to say so has to hold everything back until a newline turns up — which for a program that never prints one means holding everything back for good.

The view is one row tall and as wide as what is left of the open row, so what is drawn into it cannot run past the edge and take the block's anchor with it. A row with no room left is finished and the cells go onto the next one: appending means putting something after what is there, not squeezing it in beside it.

Drawing nothing publishes nothing, so an empty chunk costs no row.

func (*Inline) Break added in v0.0.2

func (i *Inline) Break()

Break finishes the open row, so that what is published next begins one of its own.

It writes nothing. The row was published with the block underneath it already, so ending it is only a matter of not carrying it on — which is why a caller can break a row it has changed its mind about at no cost.

func (*Inline) Cursor added in v0.0.2

func (i *Inline) Cursor() Cursor

Cursor is where the last frame asked for the terminal's cursor to go, in the block's own coordinates. See Screen.Cursor.

func (*Inline) Finish

func (i *Inline) Finish(w io.Writer) error

Finish leaves the block on screen with the cursor below it, so whatever writes next — the shell's prompt, or this program's own output — starts on a line of its own instead of on top of the interface.

It is the counterpart of giving back the alternate screen, and the reason an inline program has to draw one last frame before it exits: the last thing it showed is the thing that stays.

func (*Inline) Flush

func (i *Inline) Flush(w io.Writer) error

Flush writes this frame to w, leaving the cursor wherever the frame placed it.

A flush that would change nothing writes nothing at all, for the same reason a Screen does: an idle interface should be silent on the wire and should leave the cursor's blink undisturbed.

func (*Inline) Frame

func (i *Inline) Frame() View

Frame blanks the drawing surface and returns the view for this frame.

The view is as tall as the block may grow to, not as tall as the block ends up: its height is decided by what this frame draws into it.

func (*Inline) Invalidate

func (i *Inline) Invalidate()

Invalidate forgets what the terminal is showing, so the next flush rewrites the whole block.

func (*Inline) Print

func (i *Inline) Print(content Drawable)

Print draws rows that become part of the terminal's own output, above the interface, and stay there.

The rows are drawn now, into a surface as wide as the block, and kept as the text they came to. They reach the terminal with the next flush, before the block, which is what puts them above it.

Whole rows, each on a row of its own: a row left open by Inline.Append is finished first, because what follows it is not part of it. Output arriving in pieces that do not stop at a line boundary is that other method's business.

func (*Inline) Resize

func (i *Inline) Resize(w, h int)

Resize changes the width and the height the block may grow to.

func (*Inline) SetDepth

func (i *Inline) SetDepth(d Depth)

SetDepth says how much colour the terminal can show. It forces a full repaint, because every row the terminal is holding was encoded at the old depth.

func (*Inline) SetGround added in v0.0.2

func (i *Inline) SetGround(g Ground)

SetGround says what the terminal's own two colours are, so that a layer drawn over another can mix with it — see View.Blend.

The scratch surface is told as well as the two that swap: printed output is drawn through a view like anything else, and a block of it that dims part of itself should dim the same way there as it would in the frame.

func (*Inline) Size

func (i *Inline) Size() (w, h int)

Size returns the block's width and the height it may grow to.

func (*Inline) Tail added in v0.0.2

func (i *Inline) Tail() (col int, open bool)

Tail is how far along its row the published output has got, and whether the row is open at all. A caller laying text out for Inline.Append asks how much of the row is already spoken for; one deciding whether it owes a line break asks the second answer.

type Painter added in v0.0.3

type Painter interface {
	// Paint writes what puts this in a region of size cells, with the
	// terminal's cursor already at its top-left corner, and leaves the cursor there.
	Paint(w io.Writer, size image.Point) error
	// Erase writes what takes it off the terminal again, for a terminal that
	// remembers what it was shown, and leaves the cursor alone.
	Erase(w io.Writer) error
}

Painter is something that writes itself onto the terminal in a region of a frame, rather than into cells.

It is what a cell cannot hold: a picture, a plot drawn in pixels, anything whose contents are bytes the terminal understands and this package does not. A frame keeps room for one with View.Paint, writes the cells around it as usual, and then hands it the writer with the cursor already at the region's corner.

The one rule

Paint must leave the cursor where it found it.

That is not a nicety. A frame is written as a stream of movements from one known position to the next — an inline block's whole position is relative to where the last frame left the cursor — so a painter that moved it would move everything drawn after it. The rule is also, exactly, what makes a protocol usable in a region that redraws: the image protocol that can be told not to move the cursor is the same one that can be told to remove an image again, and the ones that cannot are the ones that only work in output that is never drawn over. See the graphics package, which says the same thing from the other side.

Erasing

Some terminals remember what they were shown. An image placed by name stays until it is taken away, so a region that has gone — scrolled off, replaced, resized — has to be unsaid rather than merely painted over, and Painter.Erase is where that is written. A painter whose output is only cells has nothing to undo and writes nothing.

type RGB

type RGB struct{ R, G, B uint8 }

RGB is a 24-bit colour.

func PaletteRGB

func PaletteRGB(index uint8) RGB

PaletteRGB is what the xterm 256-colour palette holds at an index.

The three regions are the sixteen ANSI colours, the 6×6×6 cube, and a 24-step grey ramp. Terminals may render the first sixteen however they like, so those values are what xterm uses and not a promise.

func (RGB) Blend added in v0.0.2

func (c RGB) Blend(over RGB, opacity float64) RGB

Blend mixes c toward over by opacity, clamped to [0,1].

This is the whole of compositing in a terminal. There is no alpha channel on the wire — a cell holds one background and one foreground, and both are opaque — so a translucent layer has to be resolved to opaque colours before anything is written. Doing the mixing here, on two colours that are certainly numbers, is what keeps that resolution in one place.

func (RGB) Dark added in v0.0.2

func (c RGB) Dark() bool

Dark reports whether this colour is dark enough that what goes on top of it should be light.

That, rather than "is it dark" in the abstract, is the question a theme asks when it learns what the terminal draws on. The answer weights the channels by how much of brightness the eye takes from each — green far more than blue — and puts the line down the middle. An unweighted average would call a saturated blue light and a saturated green dark, and get both backwards.

func (RGB) Index16

func (c RGB) Index16() uint8

Index16 is the nearest of the sixteen colours every terminal has.

func (RGB) Index256

func (c RGB) Index256() uint8

Index256 is the nearest entry of the xterm 256-colour palette.

Both the colour cube and the grey ramp are searched and the closer of the two wins. Searching only the cube would turn every near-grey into a muddy brown: the cube's greys are the six points where all three channels agree, and the ramp has twenty-four.

The first sixteen indices are left out of the search on purpose. A terminal is free to render those as anything at all — a theme's own palette, usually — so choosing one because its default value happened to be close is choosing a colour nobody can predict.

type Screen

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

Screen is the terminal's contents, double-buffered.

A frame is drawn into the back surface and flushed: the screen works out the smallest escape stream that turns what the terminal is showing into what was drawn, wraps it so the terminal applies it atomically, and swaps. Nothing above this type sequences escape codes, decides when to repaint, or tracks what the terminal already knows.

A flush that would change nothing writes nothing at all — not even the frame markers — because an idle UI should be silent on the wire and should leave the cursor's blink undisturbed. A Screen must not be copied after first use; its two surfaces and terminal-state model are one publication owner.

func NewScreen

func NewScreen(w, h int) *Screen

NewScreen returns a screen of the given size whose first flush repaints everything.

func (*Screen) Cursor added in v0.0.2

func (s *Screen) Cursor() Cursor

Cursor is where the last frame asked for the terminal's cursor to go.

It reads back what View.PlaceCursor recorded, which is otherwise only observable by decoding the escape stream. A caller that has to know where the caret is — to place something beside it, or to check that drawing put it where it meant to — had no way to ask before this.

func (*Screen) Flush

func (s *Screen) Flush(w io.Writer) error

Flush writes this frame to w, leaving the cursor wherever the frame placed it.

func (*Screen) Frame

func (s *Screen) Frame() View

Frame blanks the drawing surface and returns the view for this frame.

Every frame draws everything it wants to be visible. Keeping content across frames is the diff's job, not the caller's, and a surface that carried yesterday's cells forward would make a missed redraw look like success.

func (*Screen) Invalidate

func (s *Screen) Invalidate()

Invalidate forgets what the terminal is showing, so the next flush repaints in full. It is what to call after handing the terminal to another program.

func (*Screen) Resize

func (s *Screen) Resize(w, h int)

Resize changes the screen's size. The next flush repaints everything: after a resize the terminal has reflowed its own contents, and nothing about what it is showing can be assumed.

func (*Screen) SetDepth

func (s *Screen) SetDepth(d Depth)

SetDepth says how much colour the terminal can show. It forces a full repaint, because every cell the terminal is holding was encoded at the old depth.

func (*Screen) SetGround added in v0.0.2

func (s *Screen) SetGround(g Ground)

SetGround says what the terminal's own two colours are, so that a layer drawn over another can mix with it — see View.Blend.

Both surfaces are told, because a flush swaps them. No repaint is forced: the ground is read while a frame is being drawn, so the next frame uses it by drawing itself, and nothing already on the terminal was encoded with it.

func (*Screen) Size

func (s *Screen) Size() (w, h int)

Size returns the screen's width and height.

type Style

type Style struct {
	FG, BG Color
	Attr   Attr
}

Style is how a cell looks. The zero Style is the terminal's own appearance.

func (Style) Merge

func (s Style) Merge(over Style) Style

Merge lays over on top of s: whatever over states wins, whatever it leaves at its default is inherited. Attributes accumulate, because an overlay that adds emphasis should not silently drop the emphasis underneath it.

type Surface

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

Surface is a rectangle of cells in row-major order. It is storage and geometry; drawing happens through the View it hands out, so no caller has to carry a clip rectangle alongside the buffer it is clipping. A Surface must not be copied after first use: its cells and paint regions have one mutable owner.

func NewSurface

func NewSurface(w, h int) *Surface

NewSurface returns a blank surface of the given size. Negative dimensions collapse to zero. It panics with a grid error when the dimensions' product cannot be represented by int.

func (*Surface) Bounds

func (s *Surface) Bounds() image.Rectangle

Bounds is the surface's own rectangle, with its origin at zero.

func (*Surface) CellAt

func (s *Surface) CellAt(x, y int) (Cell, bool)

CellAt returns a copy of the cell at (x, y) and whether the coordinates are inside the surface. Content can only be changed by drawing through a View, which preserves complete display atoms.

func (*Surface) CopyRows

func (s *Surface) CopyRows(src *Surface, srcTop, dstTop, n int)

CopyRows copies n whole rows out of src, starting at srcTop, into s starting at dstTop. Rows that fall outside either surface are skipped, which is what lets a caller render an over-tall item into a scratch surface and lift the visible slice of it into place.

func (*Surface) Ground added in v0.0.2

func (s *Surface) Ground() Ground

Ground is what a default colour in these cells resolves to.

func (*Surface) Reset

func (s *Surface) Reset()

Reset blanks every cell and forgets the regions something else was to paint.

func (*Surface) Resize

func (s *Surface) Resize(w, h int)

Resize changes the surface's size and blanks it. Content is not preserved: every resize is followed by a full redraw, so carrying stale cells across one would only make the first frame after it wrong in a subtler way. Negative dimensions collapse to zero. Resize panics with a grid error when the dimensions' product cannot be represented by int.

func (*Surface) Row

func (s *Surface) Row(y int) []Cell

Row returns a copy of one row, or nil when y is outside the surface. A row is an inspection result, not a mutable view into the grid.

func (*Surface) Rows added in v0.0.3

func (s *Surface) Rows() []string

Rows is what the surface says, one string per row, with the styling dropped and trailing blanks cut.

func (*Surface) SetGround added in v0.0.2

func (s *Surface) SetGround(g Ground)

SetGround says what a default colour in these cells resolves to. It survives a resize and a reset, because it describes the terminal rather than the contents.

func (*Surface) Size

func (s *Surface) Size() (w, h int)

Size returns the surface's width and height.

func (*Surface) View

func (s *Surface) View() View

View returns a drawing view over the whole surface.

type View

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

View is a clipped window onto a Surface, addressed in its own coordinates.

A view bounds drawing intent to one box: coordinates are local, and an operation that meets no cell inside the clip does nothing. The clip is not a storage isolation barrier because a multi-column display atom is indivisible. Replacing any of its columns blanks the complete old atom, preserving the old style on repaired columns outside the clip. Appearance belongs to the atom's head column: an appearance area containing that head changes the complete atom, including Style and Link outside the clip; an area containing only continuations does not. These are the only mutations a view may cause beyond its clip.

The zero View draws nowhere and reports a size of zero, which is the right answer for content laid out into no space at all.

func (View) Blend added in v0.0.2

func (v View) Blend(r image.Rectangle, over Color, opacity float64)

Blend paints a translucent sheet of colour over r, in this view's coordinates: every cell's foreground and background move toward over by opacity.

This is how a layer floats above what it covers instead of erasing it. Both colours move, and by the same amount, which is what makes the region recede as a whole — text and its background keep their relationship and simply lose contrast against everything outside the sheet. Content is untouched, so what is behind stays readable and stays where it was.

A cell whose colour is the terminal's own is resolved through View.Ground first. Where that has no answer the cell keeps the colour it had, which is the rule stated on Color.Blend and the reason a program asks the terminal what it draws on before the first frame. A multi-column atom whose head is in r is blended as one glyph even when its continuations cross an edge, as described on View.

func (View) Bounds

func (v View) Bounds() image.Rectangle

Bounds is the view's own coordinate space, origin at zero.

func (View) CellAt

func (v View) CellAt(x, y int) (Cell, bool)

CellAt returns a copy of the cell at local (x, y) and whether it is inside the clip. Use drawing operations such as View.Text, View.Fill, View.MergeStyle and View.Link to change content or appearance.

func (View) Empty

func (v View) Empty() bool

Empty reports whether the view has nowhere to draw.

func (View) Fade added in v0.0.2

func (v View) Fade(r image.Rectangle, amount float64)

Fade dissolves what is in r into whatever it is drawn on: each cell's foreground moves toward that cell's own background by amount, from 0 for nothing to 1 for gone.

It is the other half of compositing and the one that takes no colour, because the colour is different in every cell and is already there. A header sliding out from under the next one, and a sweep of light along a line still arriving, are both this — and neither could be a View.Blend, because the sheet would have to be a different colour over the themed part than over the plain part.

A cell whose colours are the terminal's own is resolved through View.Ground first, and where that has no answer the cell is left alone. A multi-column atom whose head is in r is faded as one glyph even when its continuations cross an edge, as described on View.

func (View) Fill

func (v View) Fill(r image.Rectangle, style Style)

Fill blanks every cell in r, in this view's coordinates, and gives it style. An old display atom crossing an edge of the filled area is blanked completely, including any part outside the view's clip, as described on View.

func (View) Ground added in v0.0.2

func (v View) Ground() Ground

Ground is what a default colour in this view's cells resolves to.

A caller that mixes colours with what is underneath asks here. Nothing above this package carries the answer around: the view is already where drawing happens, and it already knows which terminal it is bound for.

func (v View) Link(x, y, w int, target string)

Link stamps target onto the display atoms whose heads occupy w columns starting at local (x, y), turning text that has already been written into a hyperlink. It is separate from View.Text because a link usually spans a run that was drawn in several pieces. A multi-column atom receives one link even when it crosses an edge, as described on View.

func (View) MergeStyle added in v0.0.5

func (v View) MergeStyle(x, y int, style Style) bool

MergeStyle lays style over the display atom whose head is at local (x, y), preserving any roles it already carries. It reports whether the coordinates named an atom head inside the view. A multi-column atom is restyled as one glyph even when its continuations cross the clip, as described on View.

Styling is an operation rather than a mutable Cell pointer so changing appearance cannot also replace one half of a wide grapheme.

func (View) Paint added in v0.0.3

func (v View) Paint(r image.Rectangle, id uint64, by Painter)

Paint keeps the region r of the frame for something that draws itself — see Painter.

The identity says what is being painted. Two frames that name the same thing in the same place write nothing between them, one that names something else replaces it, and one that names it nowhere takes it away; a caller with nothing to number by can pass zero, and then every frame is a different picture in the same place.

A region that does not fit entirely inside what the view may draw on is not painted at all. Half a picture, squashed into the part that fits, is worse than none: this layer knows how many cells the region has and nothing about what is in it, so it cannot crop what it cannot read.

The cells under it are left alone. What is painted goes behind them where the terminal allows it, which is what lets a caption be written over a picture.

func (View) PlaceCursor

func (v View) PlaceCursor(x, y int, style CursorStyle)

PlaceCursor asks for the terminal's cursor to sit at local (x, y) with style.

It is how the drawing owner places the cursor without anyone in between having to carry the answer: the view already knows where it sits on the screen, so the caller speaks in local coordinates and the translation is nobody's job.

A position outside what the view may draw on is ignored, for the same reason a glyph there would be: content scrolled off the screen does not get to move the cursor. A frame in which nobody places the cursor is a frame with no cursor, which is the right answer when nothing is being typed into.

func (View) Size

func (v View) Size() (w, h int)

Size returns the box the view was laid out into.

func (View) Sub

func (v View) Sub(r image.Rectangle) View

Sub returns a view onto r, expressed in this view's coordinates. Clipping only ever narrows: a caller cannot hand a child room it does not have itself. As with every View, replacing a column at the narrowed edge may blank the rest of an old display atom that crosses that edge. An appearance operation applies to a complete atom only when its head remains inside the narrowed view.

Example

A view is a clipped window addressed in its own coordinates: a widget handed one draws from (0, 0) and cannot reach outside its box, so nothing has to be told where on screen it ended up.

package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/grid"
)

func main() {
	rows := grid.Render(24, 2, func(v grid.View) {
		right := v.Sub(grid.Rect(12, 0, 12, 2))
		// Local coordinates: (0, 0) is the left edge of the sub-view, not the screen.
		right.Text(0, 0, "right half", grid.Style{})
		// Discarded rather than reported: the box is a boundary, not a convention a
		// widget could break by accident and see on screen.
		right.Text(0, 5, "below", grid.Style{})
	})
	for _, row := range rows {
		fmt.Printf("%q\n", row)
	}
}
Output:
"            right half"
""

func (View) Subs added in v0.0.5

func (v View) Subs(rects []image.Rectangle) []View

Subs returns child views for rects expressed in this view's coordinates.

It performs projection, not layout: the rectangles may come from any geometry model. Keeping that distinction lets geometry remain independent of the cell store while callers turn a complete arrangement into views in one operation.

func (View) Text

func (v View) Text(x, y int, s string, style Style) int

Text writes s at local (x, y) and returns how many columns it advanced, including any it advanced outside the clip.

Text is grapheme-aware. A multi-column cluster is never split: one that would straddle an edge is dropped and its visible columns are blanked, because part of a glyph is worse than a gap. A zero-width cluster — a combining mark arriving on its own — joins the display atom to its left instead of consuming a column of its own. Replacing an old atom that crosses a clip edge may blank the rest of that old atom outside the clip, as described on View.

Example

A multi-column cluster is never split. One that would straddle an edge is dropped and its columns are blanked, because half a glyph is worse than a gap.

package main

import (
	"fmt"

	"github.com/Tangerg/oolong/core/grid"
)

func main() {
	rows := grid.Render(5, 2, func(v grid.View) {
		// Widths chosen so the last cluster has one column and needs two.
		v.Text(0, 0, "ab中", grid.Style{})
		v.Text(0, 1, "中中中", grid.Style{})
	})
	for _, row := range rows {
		fmt.Printf("%q\n", row)
	}
}
Output:
"ab中"
"中中"

func (View) Visible

func (v View) Visible() image.Rectangle

Visible is the part of the view that will actually reach the screen, in the view's own coordinates. It is empty for a view with nowhere to draw.

Jump to

Keyboard shortcuts

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