whynot

package module
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: 17 Imported by: 0

README

Why Not?

whynot is a Go library for showing Markdown documents inside games and GUI apps. Think of patch notes, an in-game journal, help screens or a credits scroll: real formatted text, with headings, lists, tables, images and links, but without bringing in a whole UI toolkit. You give whynot some Markdown, and it lays the document out, draws it, and lets the user scroll through it and click its links. It draws through a small interface rather than a particular graphics library, and it comes with backends for Ebitengine and Gio.

whynot showing this README

  • Broad Markdown coverage. Headings, emphasis, lists, tables, blockquotes, code blocks, images (including animated GIFs) and links are all supported. Anything whynot doesn't support yet is shown as flagged text rather than breaking the document.
  • Built for long documents. whynot only lays out the part of the document that's on screen, so scrolling and resizing are just as fast for ten thousand lines as for ten.
  • Easy to drop in. A whynot.Panel shows a document in any rectangle of your window and handles its input, including a scrollbar, touch scrolling and flings.
  • Styled your way. Colors, text styles, margins and scrollbars come from a stylesheet; light and dark ones are ready-made. Text can use the bundled Go fonts, your own font files, or the fonts installed on the system.
  • Extensible code blocks. Plugins can add syntax highlighting to code blocks, or turn them into pictures, such as Mermaid diagrams.
  • Independent of any graphics library. whynot draws through a small canvas interface and receives input as plain values, so supporting another framework only takes a thin adapter.

Try it

There are two standalone document viewers built on the library, one for each backend. They behave the same way: they open Markdown files and web pages, follow links, keep a history, and support zoom and light and dark themes. They're the quickest way to see what whynot can do, and both also run in a web browser.

Viewer Install In your browser
whynot (Ebitengine) brew install arnodel/tap/whynot, a release binary, or go install github.com/arnodel/whynot/cmd/whynot@latest Try it
giowhynot (Gio) go install github.com/arnodel/whynot/backends/giobackend/cmd/giowhynot@latest Try it

Start a viewer with the path or URL of a Markdown document to open it, or with nothing to see its welcome page. For example, the screenshot above was taken with:

whynot https://raw.githubusercontent.com/arnodel/whynot/refs/heads/main/README.md

Each viewer's own README describes its keys and options, and how its web version works.

Use it in your program

Add the library to your module with:

go get github.com/arnodel/whynot
Quick start: a document in an Ebitengine game

The simplest way to show a document is a whynot.Panel. A Panel displays a document in a rectangle of your window and reacts to the mouse, the wheel and touch. Every frame, your game gives it that frame's input and the current time, and the Panel tells you what happened, such as the user clicking a link. Here is a complete program:

package main

import (
	"image"
	"log"
	"time"

	"github.com/hajimehoshi/ebiten/v2"

	"github.com/arnodel/whynot"
	"github.com/arnodel/whynot/backends/ebitenbackend"
	"github.com/arnodel/whynot/fonts"
	"github.com/arnodel/whynot/styles/simpletheme"
)

const source = "# Hello\n\nThis is **whynot**, showing [a link](https://example.com)."

type game struct {
	panel    *whynot.Panel
	renderer *ebitenbackend.Renderer // draws onto Ebitengine images
	input    ebitenbackend.Input     // reads Ebitengine's mouse, wheel and touch
	start    time.Time               // the start of the panel's clock
}

func (g *game) Update() error {
	// Pass this tick's input to the panel, which scrolls, highlights links
	// and so on, and returns what the app may want to react to.
	events := g.panel.Frame(g.input.Events(), time.Since(g.start))
	for _, e := range events {
		if link, ok := e.(whynot.LinkClick); ok {
			log.Println("clicked a link to", link.Destination)
		}
	}
	return nil
}

func (g *game) Draw(screen *ebiten.Image) {
	// Draw the panel onto the screen, as it is at this time (animated
	// images and fading scrollbars depend on it).
	g.panel.Draw(g.renderer.NewCanvas(screen), time.Since(g.start))
}

func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
	// Draw at the screen's real resolution, so text stays sharp on a
	// high-density display.
	scale := ebiten.Monitor().DeviceScaleFactor()
	w, h := int(float64(outsideWidth)*scale), int(float64(outsideHeight)*scale)
	g.panel.SetBounds(image.Rect(0, 0, w, h)) // fill the window, which may have been resized
	g.panel.SetScale(scale)                   // size text and margins for that resolution
	return w, h
}

func main() {
	doc := whynot.Parse([]byte(source))
	// A View draws a document, here in the bundled Go fonts and the dark theme.
	view := whynot.NewView(doc, fonts.NewGoSelector(), simpletheme.DarkStyleSheet)
	// The panel's bounds are set in Layout, once the window size is known.
	panel := whynot.NewPanel(view, image.Rectangle{})
	panel.SetScrollbar(true)
	g := &game{panel: panel, renderer: ebitenbackend.New(), start: time.Now()}
	if err := ebiten.RunGame(g); err != nil {
		log.Fatal(err)
	}
}

When the user clicks a link to a heading in the same document (a #heading link), the Panel scrolls there by itself. Any other link is reported to your program as a whynot.LinkClick, and it's up to you what to do with it, because only your program knows where documents come from. examples/panel shows a Panel that covers only part of a game's window.

With Gio

The Gio backend lives in its own module, so that programs which don't use Gio don't depend on it. Add it with:

go get github.com/arnodel/whynot/backends/giobackend

A Panel works the same way in a Gio window: giobackend.Input provides the input, and giobackend.Renderer provides the canvas to draw on. The one difference is that Gio only draws a new frame when something happens, so while the Panel is animating (a fling coasting to a stop, say, or a scrollbar fading out) your program must ask Gio for the next frame. The package documentation shows the whole frame loop, and examples/gio is a program you can run.

Another framework

A backend for another framework provides two things: a canvas.Canvas that draws text, images and rectangles with the framework, and an input reader that turns the framework's input into whynot's input events. Your program then runs the same frame loop as above. backends/README.md explains what each part has to do, and the two existing backends are short enough to serve as examples.

More control: View and Controller

A Panel is made of two parts, which you can also use directly when you need more control than a Panel gives you, for example to fit whynot into a structure of your own:

  • a View lays out and draws a document, scrolled to a position;
  • a Controller turns input into what it does to a View: scrolling, highlighting the link under the pointer, and reporting clicks.
view := whynot.NewView(doc, fonts.NewGoSelector(), simpletheme.DarkStyleSheet)
controller := whynot.NewController(view)

// Then, every frame:
view.SetBounds(rect)                 // the rectangle of the screen the view occupies
events := controller.Frame(in, now) // apply this frame's input; get back what happened
view.Draw(canvas, now)               // draw the view within its rectangle

Your own code can also move a View directly, without any input: ScrollBy scrolls by a distance, ScrollToAnchor jumps to a heading, and ScrollToRatio jumps to a point in the document. To draw a scrollbar of your own, VisibleRange tells you which part of the document is on screen, and HoveredLink tells you which link is under the pointer, for a status bar. examples/view shows a View on its own, scrolled with the mouse wheel. All positions are measured in the canvas's pixels; the Coordinates section of the documentation explains this in detail.

Styles

A View's appearance comes from its stylesheet. The styles/simpletheme package makes stylesheets from a set of plain fields: you can use one of its ready-made stylesheets, or start from one of its themes and change what you need, then make a stylesheet from it:

theme := simpletheme.Dark() // or simpletheme.Light()
theme.LinkColor = myColor
theme.HeadingTextStyles[0] = simpletheme.TextStyle{Size: 48, Weight: simpletheme.WeightBlack} // top-level headings
view.SetStyleSheet(theme.StyleSheet())

You can change a View's stylesheet at any time, which is all it takes to switch between a light and a dark theme.

Fonts

The stylesheet decides the size, weight and family of each piece of text, and a font selector decides which font actually draws it. whynot comes with three:

  • fonts.NewGoSelector() uses the Go fonts, which are bundled with whynot.
  • fonts.NewCustomSelector() uses font files you provide, for whichever families, weights and styles you choose, and the Go fonts for the rest. See examples/customfont.
  • The fonts/systemfont package uses fonts installed on the computer, either by name or by picking the platform's usual interface font. See examples/systemfont.
selector := systemfont.New()
// The platform's usual interface font. On an error, such as the font not
// being installed, the bundled Go fonts are used instead.
if err := selector.RegisterPreferredFont(fonts.Proportional); err != nil {
	log.Print(err)
}
// A font installed on the system, by name.
if err := selector.RegisterSystemFont(fonts.Monospace, "Menlo"); err != nil {
	log.Print(err)
}
view := whynot.NewView(doc, selector, simpletheme.DarkStyleSheet)
Code blocks: highlighting and diagrams

By default, a code block is shown as plain text in a single color. Code-block plugins change that for the languages they handle: a plugin can split the code into colored tokens, for syntax highlighting, or replace the block with an image, such as a rendered diagram. Plugins are passed to Parse, and they're tried in the order given. That order matters when a plugin produces an image: while the image loads, or if it fails to, the next plugin's version of the block is shown, such as the diagram's source, highlighted.

doc := whynot.Parse(source,
	whynot.WithCodeBlockPlugin(kroki.Plugin{}),          // draws Mermaid diagrams, using kroki.io
	whynot.WithCodeBlockPlugin(chromahighlight.Plugin{}), // highlights code, using the chroma library
)

The codeblocks package defines what a plugin is, so you can write your own. examples/chromahighlight shows syntax highlighting.

Images

Images are loaded in the background, so a slow image never holds up scrolling or drawing. By default, whynot treats an image's source as the path of a local file. To load images from somewhere else, such as relative to the URL a document came from, give the View an images.Source with the whynot.WithImageSource option.

Examples

Each of these is a small program you can run, for example with go run ./examples/panel:

Example What it shows
examples/panel A Panel covering part of an Ebitengine game's window
examples/view A View used on its own, scrolled with the mouse wheel
examples/chromahighlight Syntax highlighting in code blocks
examples/customfont Text in a font file of your choice
examples/systemfont Text in the fonts installed on the system
examples/wasm whynot running in a web page, compiled to WebAssembly
backends/giobackend/examples/gio A Panel in a Gio window

The two viewers, whynot and giowhynot, are complete applications built the same way.

How it works

First, whynot.Parse turns the Markdown into a Document, with the help of any code-block plugins, and whynot.NewView makes a View to show it, with a stylesheet and fonts that decide how it looks:

flowchart TB
    md[/"Markdown text"/]
    plugins["Code-block plugins<br/><code>chromahighlight.Plugin</code>, …"]
    parse[["<code>whynot.Parse</code>"]]
    doc["<code>whynot.Document</code>"]
    sheet["Stylesheet<br/><code>simpletheme.DarkStyleSheet</code>"]
    fonts["Fonts<br/><code>fonts.GoSelector</code>"]
    newview[["<code>whynot.NewView</code>"]]
    view["<code>whynot.View</code>"]
    md --> parse
    plugins --> parse
    parse --> doc
    doc --> newview
    sheet --> newview
    fonts --> newview
    newview --> view

Then, every frame, your program runs a loop between its graphics framework and whynot. Here it is with Ebitengine; with Gio, the same roles are played by giobackend.Input and giobackend.Canvas:

flowchart TB
    fw["Ebitengine<br/>(or another framework, such as Gio)"]
    subgraph program["Your game, every frame"]
        input["<code>ebitenbackend.Input</code>"]
        subgraph panel["<code>whynot.Panel</code>"]
            ctrl["<code>whynot.Controller</code>"]
            view["<code>whynot.View</code>"]
        end
        yours["Your code"]
        canvas["<code>ebitenbackend.Canvas</code>"]
    end
    fw -- "mouse, wheel and touch" --> input
    input -- "<code>input.Event</code> values" --> ctrl
    ctrl -- "<code>whynot.Event</code> values,<br/>such as <code>whynot.LinkClick</code>" --> yours
    ctrl -- "calls to scroll it<br/>and highlight links" --> view
    view -- "drawing calls" --> canvas
    canvas -- "draws on the screen" --> fw
  1. ebitenbackend.Input reads what the user did, and turns it into whynot's own input events (input.Event values).
  2. A whynot.Controller decides what those events do to the View: scrolling it, highlighting the link under the pointer, and so on. It returns anything your program may want to react to, such as a click on a link, as whynot.Event values.
  3. The whynot.View lays out the part of the document that's on screen, and draws it.
  4. An ebitenbackend.Canvas carries out the View's drawing on the screen image.

As the diagram shows, a whynot.Panel holds a Controller and a View together, so with a Panel your program deals with a single object, as in the quick start above. You only need the Controller and the View separately for finer control. whynot.Parse uses goldmark to read the Markdown.

The library is split into packages along these lines:

Packages What they're for
whynot Parsing, and the View, Controller and Panel
canvas, input What a backend implements: drawing, and input events
fonts, images, codeblocks What a View can be given: fonts, image loading, code-block plugins
styles/simpletheme Stylesheets and their ready-made themes
fonts/systemfont, codeblocks/chromahighlight, codeblocks/kroki Ready-made fonts and plugins
backends/ebitenbackend, backends/giobackend The two backends
internal/… The Markdown compiler, the layout engine and the image cache: not part of the API

ARCHITECTURE.md explains the design in depth: the layout model, how layout stays lazy, how the scroll position survives a resize, hit-testing, and image loading.

Features

Text

  • Headings, paragraphs, emphasis, strong text, strikethrough and inline code
  • Typographic punctuation: smart quotes, dashes and ellipses

Blocks

  • Fenced and indented code blocks, which scroll sideways when they're wider than the view
  • Syntax highlighting and diagrams in code blocks, through plugins
  • Blockquotes, including nested ones
  • Ordered and unordered lists, tight or loose, nested to any depth, and task lists
  • Tables (GitHub style), with column alignment and column widths fitted to their content, which scroll sideways when they're wider than the view
  • Thematic breaks (horizontal rules)
  • Footnotes and definition lists, which are shown as plain text for now
  • Raw HTML, which is shown as flagged source text rather than rendered

Images

  • PNG, JPEG and GIF images, including animated GIFs
  • Background loading, starting ahead of the scroll position, with a placeholder of the right size while an image loads, and its alternative text if it can't be loaded

Links

  • Links, including automatic and reference-style links, highlighted under the pointer
  • Links to headings within the document (#heading)

Viewing and interaction

  • Layout and drawing whose cost depends on what's on screen, not on the document's length
  • Smooth scrolling, with the scroll position kept when the window is resized or the style changes
  • Scrolling with the mouse wheel, by touch (with flings), or by keyboard commands (a step, a page, or sideways)
  • A built-in scrollbar styled by the stylesheet, or your own, built with VisibleRange
  • Zoom
  • Anything not supported yet is shown as flagged text, with a warning in the log, rather than breaking the document

Styling

  • Colors, text styles, line height, margins and scrollbars set by a stylesheet, which can be changed at any time; light and dark themes included
  • The bundled Go fonts, your own font files, or the system's fonts

Status

whynot follows semantic versioning. From version 1.0.0, the API of every public package in the main module is stable. Packages under internal/, and the programs in cmd/ and examples/, aren't part of that promise. The Gio backend is a separate module, and it stays at version 0 for as long as Gio does, because Gio's own types are part of its API.

Known issues are listed in ARCHITECTURE.md. Ideas for the future are tracked as GitHub issues.

Contributions are welcome: CONTRIBUTING.md explains how the project is worked on.

whynot is released under the Apache License 2.0.

Documentation

Overview

Package whynot shows Markdown documents in Go programs, such as games and GUI apps: it lays a document out, draws it, and lets the user scroll through it and click its links. It draws through a small interface rather than a particular graphics library, and receives input as plain values, so it works with any framework a backend adapts: whynot comes with backends for Ebitengine and Gio.

Overview

The package has four main types:

  • Parse turns Markdown into a Document.
  • A View lays a Document out and draws it into a rectangle of a canvas, scrolled to a position. It only lays out what's on screen, so long documents cost no more than short ones.
  • A Controller turns the user's input into what it does to a View: scrolling (by wheel, touch, scrollbar or command), highlighting the link under the pointer, and reporting clicks.
  • A Panel is a View and a Controller together, with settings that survive showing another document: the simplest way to put a document on screen.

A View is given what decides how the document looks:

Parse can be given code-block plugins (WithCodeBlockPlugin; see package codeblocks), for syntax highlighting or diagrams.

Showing a document

A backend provides two things: an input.Source that reads the user's input from the framework, and a canvas.Canvas to draw on. Here is how to use them with Ebitengine.

Once, when your program starts, make the backend's input reader and renderer, and set a Panel up:

var source ebitenbackend.Input  // reads Ebitengine's input: an input.Source
renderer := ebitenbackend.New() // keeps image textures and glyph caches across frames

doc := whynot.Parse(markdown)
view := whynot.NewView(doc, fonts.NewGoSelector(), simpletheme.DarkStyleSheet)
panel := whynot.NewPanel(view, bounds) // where it's drawn on the screen
panel.SetScale(scale)                  // the screen's density

Then, every frame, pass the input to the Panel, with the time, and react to what happened. With Ebitengine, do that in your game's Update method:

events := panel.Frame(source.Events(), now)
// react to events, such as a LinkClick

Draw the Panel every frame too. Ebitengine gives your game's Draw method the screen image to draw on each time, so make a canvas onto it each time as well. That's cheap: the renderer holds what's worth keeping.

panel.Draw(renderer.NewCanvas(screen), now)

With Gio, a giobackend.Input and a giobackend.Renderer play the same roles, and the frame loop is shaped by Gio's events instead. The packages backends/ebitenbackend and backends/giobackend each show a complete program. When the window is resized, the screen changes or the user zooms, call Panel.SetBounds, Panel.SetScale or Panel.SetZoom: the document is laid out again, keeping the scroll position.

A Panel, or a View and a Controller

A Panel covers what most programs need. Use a View and a Controller directly for more control: you can then drive a View with input of your own, fit it into a structure of your own, or move it by code alone (View.ScrollBy, View.ScrollToAnchor, View.ScrollToRatio). A View also answers questions about what it shows: the link under the pointer (View.HoveredLink), the part of the document in view (View.VisibleRange), the heading currently on screen (View.CurrentHeadingID).

Coordinates

whynot uses one coordinate space: the canvas's. The Canvas a View draws on, the input events a Controller acts on, a View's or Panel's bounds, and everything a View reports by position (View.HitTest, View.LinkAt) share the same origin and the same unit, canvas pixels: device pixels for whynot's own backends. Where the canvas sits in a window is the backend's business, not whynot's.

A View is a window onto that space: View.SetBounds says where it is, and it draws and answers only there. There is no separate coordinate space of the View's own to convert to and from: a point a Controller receives can be passed to HitTest as it is, and the rectangle HitTest returns can be drawn on the canvas as it is.

Distances are in canvas pixels too, such as View.ScrollBy's. The scale relates them to logical pixels, the unit of a StyleSheet's dimensions: it's the number of canvas pixels per logical pixel (View.SetScale), and a View is laid out at its scale times its zoom (View.SetZoom). Command scrolling (Controller.ScrollDown, Controller.ScrollLeft and the like, also on a Panel) steps in logical pixels without the zoom, so a step covers the same distance on screen at any zoom.

How far a View is scrolled into its document isn't in pixels, because the document's height is an estimate until all of it has been laid out: View.VisibleRange reports the part in view as fractions of the height, and View.ScrollToRatio scrolls to one.

Events and state

What happens in a View that a program may react to comes out of Controller.Frame as Event values, in order: the counterpart of the input events going in. They're a sealed set, like input's, so new kinds can be added without breaking anyone:

for _, e := range panel.Frame(source.Events(), now) {
	switch e := e.(type) {
	case whynot.LinkClick:
		// follow e.Destination: load another document, say
	case whynot.AnchorClick:
		// already scrolled to e.ID, unless turned off
	}
}

A Controller only reports events. A Panel also acts on one: it scrolls to an AnchorClick's heading, the way a browser follows a "#id" link, unless Panel.SetAnchorScrolling turns that off for a program that handles it itself, to record history first, say. Following a LinkClick is always up to the program.

What's true at the moment, rather than what happened, is read from the View when needed, such as the link under the pointer (View.HoveredLink), which a program may show in a status bar.

The contracts whynot works with each have a package of their own, so that each can be implemented separately:

  • canvas and input: what a backend provides, drawing and input events. The backends are under backends/.
  • fonts, images and codeblocks: fonts, image loading and code-block plugins. Ready-made implementations are in their subpackages: fonts/systemfont (fonts installed on the system), codeblocks/chromahighlight (syntax highlighting) and codeblocks/kroki (diagrams).
  • Stylesheets: styles/simpletheme, with light and dark themes.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type AnchorClick added in v0.6.0

type AnchorClick struct {
	// ID is the anchor's id, percent-decoded and without the "#", ready
	// for [View.ScrollToAnchor]. It's empty for a bare "#", which
	// browsers take to mean the top of the document.
	ID string
}

AnchorClick is a link within the document ("#id") being clicked or tapped. A Panel scrolls to the anchor by default (see Panel.SetAnchorScrolling); a Controller only reports it.

type Controller added in v0.6.0

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

Controller turns a backend's input events into what they do to a View: scrolling, link hovering, sideways-scrolling blocks and their scrollbars, touch pans and flings. Feed it each frame's input with Frame, which returns the Events that happened, such as a link being clicked. It also scrolls on command (ScrollDown, PageDown, ScrollLeft and so on), for an app binding keys to them.

func NewController added in v0.6.0

func NewController(v *View) *Controller

NewController returns a Controller for v, at scale 1. It acts on input inside v's Bounds.

func (*Controller) Animating added in v0.6.0

func (c *Controller) Animating() bool

Animating reports whether frames must keep coming even without input: while a fling coasts, or a scrollbar fades out. A backend whose frames are event-driven (like Gio's) must keep requesting frames while it's true, or the animation stops dead.

func (*Controller) Frame added in v0.6.0

func (c *Controller) Frame(events []input.Event, now time.Duration) []Event

Frame applies one frame's input events, in order, at now: elapsed time on the clock the View is drawn with (see View.Draw). Call it once per frame, with no events if there were none, so flings keep coasting.

It returns what happened that the app may want to react to, such as a LinkClick, in order; nil if nothing did. The Controller doesn't act on them itself.

A fling stops if the app moves the View itself (with View.ScrollBy, View.ScrollToAnchor and so on) or swaps it with SetView.

func (*Controller) PageDown added in v0.6.0

func (c *Controller) PageDown()

PageDown scrolls a page towards the end.

func (*Controller) PageUp added in v0.6.0

func (c *Controller) PageUp()

PageUp scrolls a page towards the start.

func (*Controller) Scale added in v0.6.0

func (c *Controller) Scale() float64

Scale returns the display's scale (see SetScale).

func (*Controller) ScrollDown added in v0.6.0

func (c *Controller) ScrollDown()

ScrollDown scrolls a step towards the end, as for an arrow key.

func (*Controller) ScrollLeft added in v0.6.0

func (c *Controller) ScrollLeft() bool

ScrollLeft scrolls a block wider than the View (a code block or table, say) a step towards its start: the block under the pointer, else the one last scrolled, clicked or touched, if it's still on screen. It reports whether there was such a block.

func (*Controller) ScrollRight added in v0.6.0

func (c *Controller) ScrollRight() bool

ScrollRight scrolls a block a step towards its end, like ScrollLeft.

func (*Controller) ScrollUp added in v0.6.0

func (c *Controller) ScrollUp()

ScrollUp scrolls a step towards the start, as for an arrow key.

func (*Controller) SetScale added in v0.6.0

func (c *Controller) SetScale(s float64)

SetScale sets the display's scale: input pixels per logical pixel, without any zoom of the document. Command scrolling steps and touch thresholds are in logical pixels, so they don't change with the zoom.

func (*Controller) SetView added in v0.6.0

func (c *Controller) SetView(v *View)

SetView makes the Controller drive v instead. Nothing stays hovered in the previous View; the next Frame hovers whatever is under the pointer in v.

func (*Controller) View added in v0.6.0

func (c *Controller) View() *View

View returns the View the Controller drives.

type Document added in v0.5.0

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

Document is a parsed Markdown document, made by Parse. It's immutable, and any number of Views can show it.

func Parse

func Parse(source []byte, opts ...ParseOption) *Document

Parse compiles Markdown source into a Document ready to render with a View. Parse only builds structure: all appearance (fonts, colors, margins, ...) comes from the View's StyleSheet, so the same Document can be shown in different styles without parsing it again.

func (*Document) TOCEntries added in v0.5.0

func (d *Document) TOCEntries() []TOCEntry

TOCEntries returns every top-level heading in the document, in document order.

func (*Document) Title added in v0.5.0

func (d *Document) Title() (string, bool)

Title returns the text of the document's first heading, at any level, or ok=false if it has none.

Only a top-level heading is found: one nested inside a blockquote or list isn't. The same holds for TOCEntries and View.ScrollToAnchor.

type Event added in v0.6.0

type Event interface {
	// contains filtered or unexported methods
}

Event is something that happened in a View that an app may react to, returned by Controller.Frame and Panel.Frame: the counterpart of the input events going in. Only this package defines kinds of Event, so new kinds can be added without breaking anyone: code that doesn't know a kind ignores it.

type LinkClick added in v0.6.0

type LinkClick struct {
	// Destination is the link's destination, as written in the document.
	Destination string
}

LinkClick is a link to elsewhere than the document itself being clicked or tapped. Following it, such as loading another document, is up to the app.

type Panel added in v0.5.0

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

Panel shows a View in a rectangle of a window and makes it interactive: it lays the View out to fit, draws it there, and turns input into scrolling, hovering and clicking (through a Controller). It keeps its settings when the View is swapped, so an app can show one document after another in the same place.

A Panel doesn't depend on a graphics library. Each frame, the app passes it the frame's input events (from a backend's input.Source) and a canvas.Canvas to draw on, along with the time.

Unlike a Controller, a Panel acts on some Events itself: it follows links within the document (see SetAnchorScrolling).

func NewPanel added in v0.6.0

func NewPanel(view *View, bounds image.Rectangle) *Panel

NewPanel returns a Panel showing view in bounds, at scale 1 and zoom 1. bounds is in canvas coordinates (see Coordinates in the package documentation).

func (*Panel) Animating added in v0.6.0

func (p *Panel) Animating() bool

Animating reports whether the Panel needs more frames even without input: a fling is coasting, or a scrollbar is fading.

func (*Panel) Bounds added in v0.5.0

func (p *Panel) Bounds() image.Rectangle

Bounds returns where the Panel is drawn on the canvas.

func (*Panel) Draw added in v0.6.0

func (p *Panel) Draw(dst canvas.Canvas, now time.Duration)

Draw draws the View onto dst, within the Panel's bounds, as it is at now.

func (*Panel) Frame added in v0.6.0

func (p *Panel) Frame(events []input.Event, now time.Duration) []Event

Frame applies one frame's input events at now, elapsed time on the app's clock, and returns what happened, like Controller.Frame. Call it once per frame, before Draw, with no events if there were none, so flings keep coasting.

Unless SetAnchorScrolling turned it off, an AnchorClick has already scrolled the View to its anchor by the time Frame returns it.

func (*Panel) PageDown added in v0.5.0

func (p *Panel) PageDown()

PageDown scrolls a page towards the end.

func (*Panel) PageUp added in v0.5.0

func (p *Panel) PageUp()

PageUp scrolls a page towards the start.

func (*Panel) Scale added in v0.5.0

func (p *Panel) Scale() float64

Scale returns the display's scale (see SetScale).

func (*Panel) ScrollDown added in v0.5.0

func (p *Panel) ScrollDown()

ScrollDown scrolls a step towards the end, as for an arrow key.

func (*Panel) ScrollLeft added in v0.6.0

func (p *Panel) ScrollLeft() bool

ScrollLeft scrolls a block wider than the View a step towards its start (see Controller.ScrollLeft), reporting whether there was one.

func (*Panel) ScrollRight added in v0.6.0

func (p *Panel) ScrollRight() bool

ScrollRight scrolls a block wider than the View a step towards its end, like ScrollLeft.

func (*Panel) ScrollUp added in v0.5.0

func (p *Panel) ScrollUp()

ScrollUp scrolls a step towards the start, as for an arrow key.

func (*Panel) SetAnchorScrolling added in v0.6.0

func (p *Panel) SetAnchorScrolling(on bool)

SetAnchorScrolling sets whether a link within the document ("#id") scrolls the View to its anchor, as it does by default. Turn it off to handle AnchorClick events differently, e.g. to record history first.

func (*Panel) SetBounds added in v0.5.0

func (p *Panel) SetBounds(r image.Rectangle)

SetBounds moves or resizes the Panel.

func (*Panel) SetScale added in v0.5.0

func (p *Panel) SetScale(s float64)

SetScale sets the display's scale: canvas pixels per logical pixel.

func (*Panel) SetScrollbar added in v0.6.0

func (p *Panel) SetScrollbar(on bool)

SetScrollbar shows or hides the View's own scrollbar (see WithScrollbar), in the current View and, when on, in every View the Panel shows after it.

func (*Panel) SetStyleSheet added in v0.5.0

func (p *Panel) SetStyleSheet(s StyleSheet)

SetStyleSheet sets the StyleSheet of the current View and of every View the Panel shows after it.

func (*Panel) SetView added in v0.5.0

func (p *Panel) SetView(v *View)

SetView shows v instead, with the Panel's bounds, scale, zoom, StyleSheet and scrollbar, if set, so it can be scrolled straight away.

func (*Panel) SetZoom added in v0.6.0

func (p *Panel) SetZoom(zoom float64)

SetZoom magnifies the document by zoom (1 is 100%), in the current View and every View the Panel shows after it (see View.SetZoom). Scrolling steps don't change with the zoom.

func (*Panel) View added in v0.5.0

func (p *Panel) View() *View

View returns the View the Panel shows.

func (*Panel) Zoom added in v0.6.0

func (p *Panel) Zoom() float64

Zoom returns the document's zoom (see SetZoom).

type ParseOption added in v0.3.0

type ParseOption func(*parseOptions)

ParseOption customizes Parse - see WithCodeBlockPlugin.

func WithCodeBlockPlugin added in v0.5.0

func WithCodeBlockPlugin(p codeblocks.Plugin) ParseOption

WithCodeBlockPlugin registers a plugin that parses fenced code blocks in the languages it handles, e.g. to color them or render them as diagrams. Call it once per plugin: plugins are tried in registration order (see codeblocks.Plugin).

type ScrollPosition

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

ScrollPosition is an opaque snapshot of a View's scroll position, captured by ScrollPosition and restored by RestoreScrollPosition.

type StyleSheet

type StyleSheet interface {
	// Styles is for whynot's own use. Its result type is internal, so a
	// StyleSheet can only be made by packages in this module.
	Styles() styling.Styles
}

StyleSheet is how a document looks: an immutable, opaque value, made by the packages under styles/ (e.g. styles/simpletheme's DarkStyleSheet). A View asked for a color reads it from its StyleSheet (see View.HighlightColor, View.ScrollbarColor).

type TOCEntry added in v0.5.0

type TOCEntry struct {
	ID    string // the heading's anchor id - what View.ScrollToAnchor takes
	Level int    // 1-6
	Text  string
}

TOCEntry is one heading of a Document - enough to build a table of contents entry linking to ID.

type View

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

View renders a Document into a rectangle of a Canvas, its Bounds, scrolled to a position. It lays out only what's needed to draw, lays out again only when the width, scale or StyleSheet change, and keeps the scroll position across those changes.

A View reads no input itself: a Controller turns input into hovering, clicking and scrolling. Its own methods are for programmatic use: SetBounds, SetScale and SetZoom when the window, screen or zoom change, and positioning with ScrollBy, ScrollToRatio, ScrollToAnchor and RestoreScrollPosition.

All its positions and distances are in canvas pixels (see Coordinates in the package documentation).

func NewView

func NewView(doc *Document, faceSelector fonts.FaceSelector, styleSheet StyleSheet, opts ...ViewOption) *View

NewView returns a View of doc (see Parse), drawn with faceSelector's fonts in styleSheet's style (e.g. simpletheme.DarkStyleSheet), at scale 1 and zoom 1. It has empty bounds, so it shows nothing until SetBounds is called.

func (*View) Bounds added in v0.6.0

func (v *View) Bounds() image.Rectangle

Bounds returns where the View is drawn on the canvas.

func (*View) CurrentHeadingID added in v0.5.0

func (v *View) CurrentHeadingID() (id string, ok bool)

CurrentHeadingID returns the id of the top-level heading at or just above the top of the viewport - the section currently on screen - or ok=false if there's none before the scroll position, or nothing has been laid out yet.

func (*View) Document added in v0.5.0

func (v *View) Document() *Document

Document returns the Document this View renders.

func (*View) Draw

func (v *View) Draw(dst canvas.Canvas, now time.Duration)

Draw draws the View onto dst, within its Bounds, as it is at now: elapsed time on a clock of the caller's choosing (any origin, as long as it's the same on every call), which animated images and fading scrollbars follow. Call it every frame: it also picks up images that have finished loading.

Draw fills its Bounds with the StyleSheet's background color first, so a caller doesn't need its own clear step. Its cost follows what's visible, not the document's size or how far into it the scroll position is.

func (*View) HighlightColor added in v0.6.0

func (v *View) HighlightColor() color.Color

HighlightColor is the color the View's StyleSheet shows a hovered link in - e.g. for an app echoing the link's destination elsewhere, in the same color.

func (*View) HitTest

func (v *View) HitTest(x, y int) (r image.Rectangle, ok bool)

HitTest returns the rectangle of the content at (x, y), e.g. for drawing an outline around it. ok is false if (x, y) is outside the Bounds, or doesn't land on any content: past the end of the document, or in a margin or gap.

func (v *View) HoveredLink() (destination string, ok bool)

HoveredLink returns the destination of the link under the pointer, as written in the document, or ok=false if there's none. A Controller keeps it up to date; it's highlighted in the StyleSheet's HighlightColor.

func (*View) LinkAt

func (v *View) LinkAt(x, y int) (destination string, ok bool)

LinkAt reports the destination URL of the link at (x, y). ok is false if (x, y) doesn't land on a link.

func (*View) RestoreScrollPosition

func (v *View) RestoreScrollPosition(p ScrollPosition)

RestoreScrollPosition restores a position captured earlier by ScrollPosition - only meaningful on the same View it was taken from.

func (*View) Scale added in v0.6.0

func (v *View) Scale() float64

Scale returns the display's scale (see SetScale).

func (*View) ScrollBy added in v0.6.0

func (v *View) ScrollBy(dy float64)

ScrollBy moves the scroll position dy pixels towards the end of the document (towards the start if dy is negative).

func (*View) ScrollPosition

func (v *View) ScrollPosition() ScrollPosition

ScrollPosition captures the View's current scroll position.

func (*View) ScrollToAnchor

func (v *View) ScrollToAnchor(id string) bool

ScrollToAnchor scrolls to put the heading with the given anchor id at the top of the viewport, e.g. after following a link with a URL fragment. ok is false, and the scroll position unchanged, if no heading has that id or nothing has been laid out yet.

Only top-level headings are found: not one nested in a blockquote or list.

func (*View) ScrollToRatio

func (v *View) ScrollToRatio(ratio float64)

ScrollToRatio puts the top of the View at ratio (clamped to [0, 1]) of the document's estimated height, e.g. while dragging a scrollbar thumb (VisibleRange gives the ratio back).

func (*View) ScrollbarColor added in v0.6.0

func (v *View) ScrollbarColor(hover, pressed bool) color.Color

ScrollbarColor is the color the View's StyleSheet gives a scrollbar thumb: hover whenever it's highlighted (including while dragged), pressed only while dragged - for a backend drawing the View's scrollbar.

func (*View) SetBounds added in v0.6.0

func (v *View) SetBounds(r image.Rectangle)

SetBounds sets where the View is drawn on the canvas, which is also where its HitTest and LinkAt apply. A change of width lays the document out again, keeping the scroll position.

func (*View) SetScale added in v0.6.0

func (v *View) SetScale(scale float64)

SetScale sets the display's scale: canvas pixels per logical pixel, so that text and margins are sized for the screen's density. The document is laid out at the scale times the zoom (see SetZoom): fonts at that times 72 DPI, and the StyleSheet's dimensions multiplied by it. A change lays the document out again, keeping the scroll position.

func (*View) SetScrollbar added in v0.6.0

func (v *View) SetScrollbar(on bool)

SetScrollbar shows or hides the View's own vertical scrollbar (see WithScrollbar).

func (*View) SetStyleSheet

func (v *View) SetStyleSheet(s StyleSheet)

SetStyleSheet swaps the View's StyleSheet and takes effect immediately: style values are resolved when the layout is built, so it's rebuilt here, re-anchoring the scroll position the same way a resize does. If nothing has been laid out yet, SetBounds lays it out with s.

func (*View) SetZoom added in v0.6.0

func (v *View) SetZoom(zoom float64)

SetZoom magnifies the document by zoom (1 is 100%), on top of the display's scale (see SetScale). A change lays the document out again, keeping the scroll position.

func (*View) VisibleRange added in v0.6.0

func (v *View) VisibleRange() (start, end float64)

VisibleRange returns the part of the document in view, as fractions of its height: from start to end, with 0 <= start <= end <= 1 - e.g. for drawing a scrollbar thumb. ScrollToRatio(start) is the way back. The height is an estimate until the whole document has been laid out, so the fractions can shift a little as more of it is seen. Before anything is laid out, it's (0, 1).

func (*View) Zoom added in v0.6.0

func (v *View) Zoom() float64

Zoom returns the document's zoom (see SetZoom).

type ViewOption

type ViewOption func(*View)

ViewOption customizes a View at construction, via NewView's opts parameter.

func WithImageSource

func WithImageSource(s images.Source) ViewOption

WithImageSource sets where the View's images come from, instead of the default images.FileSource - e.g. relative to the document's location, or over http(s). Each image is fetched at most once per View.

func WithScrollbar added in v0.6.0

func WithScrollbar() ViewOption

WithScrollbar shows the View's own vertical scrollbar, drawn along its right edge as its StyleSheet says, whenever the document is taller than the View. A Controller lets the user drag it.

Directories

Path Synopsis
backends
ebitenbackend
Package ebitenbackend is whynot's backend for Ebitengine (github.com/hajimehoshi/ebiten/v2).
Package ebitenbackend is whynot's backend for Ebitengine (github.com/hajimehoshi/ebiten/v2).
giobackend module
Package canvas is the drawing contract between whynot and a graphics framework.
Package canvas is the drawing contract between whynot and a graphics framework.
cmd
whynot command
Package codeblocks is the contract between whynot's Markdown compiler and code-block plugins, which change how fenced code blocks are shown.
Package codeblocks is the contract between whynot's Markdown compiler and code-block plugins, which change how fenced code blocks are shown.
chromahighlight
Package chromahighlight is a code-block plugin (codeblocks.Plugin) that highlights code in every language the chroma library (github.com/alecthomas/chroma) knows: a fenced code block's source is split into tokens classed as keywords, types, strings and so on, which the stylesheet colors.
Package chromahighlight is a code-block plugin (codeblocks.Plugin) that highlights code in every language the chroma library (github.com/alecthomas/chroma) knows: a fenced code block's source is split into tokens classed as keywords, types, strings and so on, which the stylesheet colors.
kroki
Package kroki is a code-block plugin that shows diagrams as images: a fenced code block in a diagram language is sent to Kroki (https://kroki.io), a service that renders diagrams, and shown as the picture it returns.
Package kroki is a code-block plugin that shows diagrams as images: a fenced code block in a diagram language is sent to Kroki (https://kroki.io), a service that renders diagrams, and shown as the picture it returns.
examples
chromahighlight command
Command chromahighlight is a runnable example of a code-block plugin: chromahighlight.Plugin (github.com/alecthomas/chroma/v2) coloring code.
Command chromahighlight is a runnable example of a code-block plugin: chromahighlight.Plugin (github.com/alecthomas/chroma/v2) coloring code.
customfont command
Command customfont is a runnable example of fonts.CustomSelector.
Command customfont is a runnable example of fonts.CustomSelector.
panel command
Command panel is a runnable example of a whynot.Panel embedded in a larger Ebitengine game window: it fills the whole window with a solid color standing in for "other game content," then draws a Panel into an inset rectangle, showing its drawing stays within its bounds on all four edges.
Command panel is a runnable example of a whynot.Panel embedded in a larger Ebitengine game window: it fills the whole window with a solid color standing in for "other game content," then draws a Panel into an inset rectangle, showing its drawing stays within its bounds on all four edges.
systemfont command
Command systemfont is a runnable example of systemfont.Selector.
Command systemfont is a runnable example of systemfont.Selector.
view command
Command view is a runnable example of whynot.View wired up by hand - full control over input handling, at the cost of doing it yourself.
Command view is a runnable example of whynot.View wired up by hand - full control over input handling, at the cost of doing it yourself.
wasm command
Command wasm is a runnable example of whynot compiled to WebAssembly and run in a browser, via Ebitengine's own js/wasm backend - see README.md in this directory for how to build and serve it.
Command wasm is a runnable example of whynot compiled to WebAssembly and run in a browser, via Ebitengine's own js/wasm backend - see README.md in this directory for how to build and serve it.
Package fonts is how whynot gets the fonts it draws text with.
Package fonts is how whynot gets the fonts it draws text with.
systemfont
Package systemfont is a fonts.FaceSelector for the fonts installed on the system.
Package systemfont is a fonts.FaceSelector for the fonts installed on the system.
Package images is how whynot gets the images a document shows.
Package images is how whynot gets the images a document shows.
Package input is the contract between a backend's input and whynot.
Package input is the contract between a backend's input and whynot.
internal
ast
Package ast is the semantic tree of a whynot document: what each element is (its Tag), how elements nest, and the few facts about them the engine needs, like a heading's anchor ID or a link's destination.
Package ast is the semantic tree of a whynot document: what each element is (its Tag), how elements nest, and the few facts about them the engine needs, like a heading's anchor ID or a link's destination.
browser
Package browser is the backend-agnostic half of a whynot document browser: navigation history, the current theme/zoom, document/image loading, the welcome page, and toolbar icons - everything cmd/whynot needs that isn't specific to ebiten, shaped so a Gio-based app can use it too.
Package browser is the backend-agnostic half of a whynot document browser: navigation history, the current theme/zoom, document/image loading, the welcome page, and toolbar icons - everything cmd/whynot needs that isn't specific to ebiten, shaped so a Gio-based app can use it too.
canvastest
Package canvastest provides a canvas.Canvas fake for tests: Recorder records each draw call instead of drawing.
Package canvastest provides a canvas.Canvas fake for tests: Recorder records each draw call instead of drawing.
engine
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.
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.
imagecache
Package imagecache loads the images a document shows through an images.Source: an asynchronous cache that fetches and decodes each image once, and decoding of animated GIFs.
Package imagecache loads the images a document shows through an images.Source: an asynchronous cache that fetches and decodes each image once, and decoding of animated GIFs.
markdown
Package markdown is whynot's Markdown compiler: it parses source with goldmark and builds the engine's block and inline tree, alongside the semantic ast.Node tree they refer to (Compile).
Package markdown is whynot's Markdown compiler: it parses source with goldmark and builds the engine's block and inline tree, alongside the semantic ast.Node tree they refer to (Compile).
styling
Package styling is whynot's style machinery: the per-element queries the layout engine resolves appearance through (Styles), and the field-configured implementation simpletheme builds (Basic).
Package styling is whynot's style machinery: the per-element queries the layout engine resolves appearance through (Styles), and the field-configured implementation simpletheme builds (Basic).
styling/stylingtest
Package stylingtest provides styles for tests of packages that can't import a theme package (whynot's own tests: styles/...
Package stylingtest provides styles for tests of packages that can't import a theme package (whynot's own tests: styles/...
styles
simpletheme
Package simpletheme makes stylesheets (whynot.StyleSheet) from a fixed set of fields: margins, text styles and colors for each kind of Markdown element, plus the scrollbars and the colors of highlighted code.
Package simpletheme makes stylesheets (whynot.StyleSheet) from a fixed set of fields: margins, text styles and colors for each kind of Markdown element, plus the scrollbars and the colors of highlighted code.

Jump to

Keyboard shortcuts

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