Documentation

Overview

package r is the core retort package

TODO: Explain how this all works.

TODO: link to the relevant files and what they do.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func CreateElement

func CreateElement(
	component Component,
	props Properties,
	children Children,
) *fiber

CreateElement is used to create the building blocks of a retort application, and the thing that Components are ultimately made up of, Elements.

import (
  "github.com/gdamore/tcell"
  "retort.dev/r"
  "retort.dev/r/component"
)

// Wrapper is a simple component that wraps the
// children Components in a box with a white border.
func Wrapper(p r.Properties) r.Element {
  children := r.GetProperty(
    p,
    r.Children{},
    "Container requires r.Children",
  ).(r.Children)

   return r.CreateElement(
    component.Box,
    r.Properties{
        component.BoxProps{
          Border: component.Border{
            Style:      component.BorderStyleSingle,
            Foreground: tcell.ColorWhite,
          },
        },
      },
    children,
  )
}

By creating an Element and passing Properties and Children seperately, retort can keep track of the entire tree of Components, and decide when to compute which parts, and in turn when to render those to the screen.

func CreateFragment

func CreateFragment(children Children) *fiber

CreateFragment is like CreateElement except you do not need a Component or Properties. This is useful when you need to make Higher Order Components, or other Components that wrap or compose yet more Components.

func CreateScreenElement

func CreateScreenElement(
	calculateLayout CalculateLayout,
	render RenderToScreen,
	props Properties,
	children Children,
) *fiber

CreateScreenElement is like a normal Element except it has the ability to render output to the screen.

Once retort has finished calculating which components have changed all those with changes are passed to a render function. This walks the tree and finds ScreenElements and calls their RenderToScreen function, passing in the current Screen.

RenderToScreen needs to return a BlockLayout, which is used among other things to direct Mouse Events to the right Component.

func Box(p r.Properties) r.Element {
	return r.CreateScreenElement(
		func(s tcell.Screen) r.BlockLayout {
			return BlockLayout
		},
		nil,
	)
}

func Retort

func Retort(root Element, config RetortConfiguration)

Retort is called with your root Component and any optional configuration to begin running retort.

func Example_app() {
  // Call the main function on retort to start the app,
  // when you call this, retort will take over the screen.
  r.Retort(
    // Root Element
    r.CreateElement(
      example.ClickableBox,
      r.Properties{
        component.BoxProps{
          Width:  100, // Make the root element fill the screen
          Height: 100, // Make the root element fill the screen
          Border: component.Border{
            Style:      component.BorderStyleSingle,
            Foreground: tcell.ColorWhite,
          },
        },
      },
      r.Children{
        // First Child
        r.CreateElement(
          example.ClickableBox,
          r.Properties{
            component.BoxProps{
              Border: component.Border{
                Style:      component.BorderStyleSingle,
                Foreground: tcell.ColorWhite,
              },
            },
          },
          nil, // Pass nil as the third argument if there are no children
        ),
        // Second Child
        r.CreateElement(
          example.ClickableBox,
          r.Properties{
            component.BoxProps{
              Border: component.Border{
                Style:      component.BorderStyleSingle,
                Foreground: tcell.ColorWhite,
              },
            },
          },
          nil,
        ),
      },
    ),
    // Pass in optional configuration
    r.RetortConfiguration{}
  )
}

func UseEffect

func UseEffect(effect Effect, deps EffectDependencies)

UseEffect is a retort hook that can be called in your Component to run side effects.

Where UseState allows your components to re-render when their State changes, UseEffect allows you to change that state when you need to.

Data fetching is a good example of when you would want something like UseEffect.

Example

The example below is a reasonably simple one that changes the color of the border of a box ever 2 seconds. The point here is to show how you can run a goroutine in the UseEffect callback, and clean up the channel in the EffectCancel return function.

import (
 "time"

 "github.com/gdamore/tcell"
 "retort.dev/component"
 "retort.dev/r"
)

type EffectExampleBoxState struct {
  Color tcell.Color
}

func EffectExampleBox(p r.Properties) r.Element {
  boxProps := p.GetProperty(
    component.BoxProps{},
    "Container requires ContainerProps",
  ).(component.BoxProps)

  children := p.GetProperty(
    r.Children{},
    "Container requires r.Children",
  ).(r.Children)

  s, setState := r.UseState(r.State{
    EffectExampleBoxState{Color: boxProps.Border.Foreground},
  })
  state := s.GetState(
    EffectExampleBoxState{},
  ).(EffectExampleBoxState)

  r.UseEffect(func() r.EffectCancel {
    ticker := time.NewTicker(2 * time.Second)

    done := make(chan bool)

    go func() {
      for {
        select {
        case <-done:
          return
        case <-ticker.C:
          setState(func(s r.State) r.State {
            ms := s.GetState(
              EffectExampleBoxState{},
            ).(EffectExampleBoxState)

            color := tcell.ColorGreen
            if ms.Color == tcell.ColorGreen {
              color = tcell.ColorBlue
            }

            if ms.Color == tcell.ColorBlue {
              color = tcell.ColorGreen
            }

            return r.State{EffectExampleBoxState{
              Color: color,
            },
            }
          })
        }
      }
    }()
    return func() {
      <-done
    }
  }, r.EffectDependencies{})

  // var mouseEventHandler r.MouseEventHandler
  mouseEventHandler := func(e *tcell.EventMouse) {
    color := tcell.ColorGreen
    if state.Color == tcell.ColorGreen {
      color = tcell.ColorBlue
    }

    if state.Color == tcell.ColorBlue {
      color = tcell.ColorGreen
    }

    setState(func(s r.State) r.State {
      return r.State{EffectExampleBoxState{
        Color: color,
      },
      }
    })
  }

  boxProps.Border.Foreground = state.Color

  return r.CreateElement(
    component.Box,
    r.Properties{
      boxProps,
      mouseEventHandler,
    },
    children,
  )
}

func UseQuit

func UseQuit() func()

UseQuit returns a single function that when invoked will exit the application

func UseScreen

func UseScreen() tcell.Screen

UseScreen returns a tcell.Screen allowing you to read and interact with the Screen directly.

Even though this means you can modify the Screen from anywhere, just as you should avoid DOM manipulation directly in React, you should avoid manipulating the Screen with this hook.

Use this hook to read information from the screen only.

If you need to write to the Screen, use a ScreenElement. This ensures when your Component has changes, retort will call your DisplayCommand function. Doing this any other way will gaurentee at some point things will get out of sync.

func UseState

func UseState(initial State) (State, SetState)

UseState provides local state for a Component.

With UseState you can make your components interactive, and repsonsive to either user input or anything else that changes.

UseState by itself only gives you the ability to change state, you generally need to pair this with either an EventHandler or UseEffect to provide interactivity.

Don't call setState inside your Component, as this will create an infinite rendering loop.

Example

The following example shows how you can use state to change the color of a Box border when it's clicked.

import (
  "github.com/gdamore/tcell"
  "retort.dev/component"
  "retort.dev/r/debug"
  "retort.dev/r"
)

type MovingBoxState struct {
  Color tcell.Color
}

func ClickableBox(p r.Properties) r.Element {
  boxProps := p.GetProperty(
    component.BoxProps{},
    "Container requires ContainerProps",
  ).(component.BoxProps)

  children := p.GetProperty(
    r.Children{},
    "Container requires r.Children",
  ).(r.Children)

  s, setState := r.UseState(r.State{
    MovingBoxState{Color: boxProps.Border.Foreground},
  })
  state := s.GetState(
    MovingBoxState{},
  ).(MovingBoxState)

  mouseEventHandler := func(e *tcell.EventMouse) {
    debug.Log("mouseEventHandler", e, state)
    color := tcell.ColorGreen
    if state.Color == tcell.ColorGreen {
      color = tcell.ColorBlue
    }

    if state.Color == tcell.ColorBlue {
      color = tcell.ColorGreen
    }

    setState(func(s r.State) r.State {
      debug.Log("mouseEventHandler update state", e, state)

      return r.State{MovingBoxState{
        Color: color,
      },
      }
    })
  }

  boxProps.Border.Foreground = state.Color

  return r.CreateElement(
    component.Box,
    r.Properties{
      boxProps,
      mouseEventHandler,
    },
    children,
  )
}

Types

type Action

type Action = func(s State) State

type ActionCreator

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

type BlockLayout

type BlockLayout struct {
	X, Y, Rows, Columns int

	Padding, Border, Margin EdgeSizes

	// Grow, like flex-grow
	// TODO: better docs
	Grow int

	// ZIndex is the layer this Box is printed on.
	// Specifically, it determines the order of painting on the screen, with
	// higher numbers being painted later, and appearing on top.
	// This is also used to direct some events, where the highest zindex is used.
	ZIndex int

	// Order is set to control the display order of a group of children
	Order int

	// Fixed if the Rows, Columns are an explicit fixed size, else they're fluid
	FixedRows, FixedColumns bool

	// Valid is set when the BlockLayout has been initialised somewhere
	// if it's false, it means we've got a default
	Valid bool
}

BlockLayout is used by ScreenElements to determine the exact location to calculate/render from. It represents the concrete positioning information specific to the size of the terminal screen.

You never set this directly, it's calculated via a component like Box. Which allows for more expressive objects, with padding, and margin.

It is recalculated when the screen is resized.

This layout information is also used to calculate which elements mouse events effect.

You shouldn't use this except for a call to r.CreateScreenElement

type BlockLayouts

type BlockLayouts = []BlockLayout

type CalculateLayout

type CalculateLayout func(
	s tcell.Screen,
	stage CalculateLayoutStage,
	parentBlockLayout BlockLayout,
	children BlockLayouts,
) (
	blockLayout BlockLayout,
	innerBlockLayout BlockLayout,
	childrenBlockLayouts BlockLayouts,
)

CalculateLayout

childrenBlockLayouts will be empty until at least CalculateLayoutStageWithChildren

innerBlockLayout is the draw area for children blocks, and will be smaller due to padding or border effects

type CalculateLayoutStage

type CalculateLayoutStage int
const (
	// Initial Pass
	// Calculate implicit or explicit absolute bounds
	CalculateLayoutStageInitial CalculateLayoutStage = iota

	// After this Blocks children have calculated their layouts
	// we recalculate this blocks layou
	CalculateLayoutStageWithChildren

	// Final Pass
	CalculateLayoutStageFinal
)

type Children

type Children []*fiber

Children is a slice of Elements (or pointers to a fiber) It's used in r.CreateElement or r.CreateFragment to specify the child nodes of the Element. It can also be extracted from props with GetProperty or GetOptionalProperty, if you want to pass children on.

In general, unless you're creating a ScreenElement, you should pass any children passed into props on to the return Element.

type Component

type Component func(props Properties) Element

Component is main thing you will be making to create a retort app. Your component must match this function signature.

type Context

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

func CreateContext

func CreateContext(initial State) *Context

CreateContext allows you to create a Context that can be used with UseContext It must be called from outside your Component.

func (*Context) Mount

func (c *Context) Mount(state State)

type DisplayCommand

type DisplayCommand struct {
	RenderToScreen *RenderToScreen
	BlockLayout    BlockLayout
}

type DisplayList

type DisplayList []DisplayCommand

DisplayList https://en.wikipedia.org/wiki/Display_list

func (DisplayList) Sort

func (dl DisplayList) Sort()

Sort a DisplayList for rendering to screen, with respect to ZIndexes

type DisplayListSortZIndex

type DisplayListSortZIndex []DisplayCommand

func (DisplayListSortZIndex) Len

func (dl DisplayListSortZIndex) Len() int

func (DisplayListSortZIndex) Less

func (dl DisplayListSortZIndex) Less(i, j int) bool

func (DisplayListSortZIndex) Swap

func (dl DisplayListSortZIndex) Swap(i, j int)

type EdgeSizes

type EdgeSizes struct {
	Top    int
	Right  int
	Bottom int
	Left   int
}

type Effect

type Effect func() EffectCancel

Effect is the function type you pass to UseEffect.

It must return an EffectCancel, even if there is nothing to clean up.

In the Effect you can have a routine to do something (such as fetching data), and then call SetState from a UseState hook, to update your Component.

type EffectCancel

type EffectCancel func()

EffectCancel is a function that must be returned by your Effect, and is called when the effect is cleaned up or canceled. This allows you to finish anything you were doing such as closing channels, connections or files.

type EffectDependencies

type EffectDependencies []interface{}

EffectDependencies lets you pass in what your Effect depends upon. If they change, your Effect will be re-run.

type Element

type Element *fiber

Element is the smallest building block of retort. You create them with r.CreateElement or r.CreateFragment

Internally they are a pointer to a fiber, which is used to keep track of the render tree.

type EventHandler

type EventHandler = func(e *tcell.Event)

EventHandler is a Property you can add to a Component that will be called on every *tcell.Event that is created.

Use this sparingly as it's very noisy.

type EventHandlerMouse

type EventHandlerMouse = func(e *tcell.EventMouse)

EventHandlerMouse is a Property you can add to a Component to be called when a *tcell.EventMouse is created.

type EventHandlerMouseHover

type EventHandlerMouseHover = func()

EventHandlerMouseHover is called when a mouse is over your Component

type EventMouseClick

type EventMouseClick = func(
	isPrimary,
	isSecondary bool,
	buttonMask tcell.ButtonMask,
) EventMouseClickRelease

EventMouseClick is called when a mouse clicks on your component. For conveince we pass isPrimary and isSecondary as aliases for Button1 and Button2.

type EventMouseClickDrag

type EventMouseClickDrag = func()

EventMouseClickDrag is not yet implemented, but could be called to allow a component to render a version that is being dragged around

type EventMouseClickRelease

type EventMouseClickRelease = func()

EventMouseClickRelease is called when the mouse click has been released. TODO: this can probably be enhanced to enable drag and drop

type EventMouseScroll

type EventMouseScroll = func(up, down, left, right bool)

type Properties

type Properties []interface{}

Properties are immutable state that is passed into a component, and pass down to components to share data.

Properties is ultimately a slice of interfaces, which lets you and retort and any components your using add any concrete structs to it. Because of this, there are some helper methods to retrieve props. These are GetProperty and GetOptionalProperty.

Properties can only contain one struct of a given type. In this sense the type of the struct is a key.

Sometimes called props.

func AddPropsIfNone

func AddPropsIfNone(props Properties, prop interface{}) Properties

AddPropsIfNone will add the prop to props is no existing prop of that type is found.

func ReplaceProps

func ReplaceProps(props Properties, prop interface{}) Properties

ReplaceProps by replacing with the same type you passed.

func (Properties) GetOptionalProperty

func (props Properties) GetOptionalProperty(
	propType interface{},
) interface{}

GetOptionalProperty will search props for the Property matching the type of struct you passed in. If it was not in props, the struct passed into propType will be returned.

You need to cast the return type of the function exactly the same as the struct you pass in.

This allows you to specify a defaults for a property.

In the following example if Wrapper is not passed a Property of the type component.BoxProps, the default values provided will be used.

func Wrapper(p r.Properties) r.Element {
  boxProps := p.GetOptionalProperty(
    component.BoxProps{
      Border: component.Border{
        Style:      component.BorderStyleSingle,
        Foreground: tcell.ColorWhite,
      },
    },
  ).(component.BoxProps)

   return r.CreateElement(
    component.Box,
    r.Properties{
        boxProps
      },
    children,
  )
}

func (Properties) GetProperty

func (props Properties) GetProperty(
	propType interface{},
	errorMessage string,
) interface{}
        Border: component.Border{
          Style:      component.BorderStyleSingle,
          Foreground: tcell.ColorWhite,
        },
      },
    },
  children,
)

}

type RenderToScreen

type RenderToScreen func(
	s tcell.Screen,
	blockLayout BlockLayout,
)

RenderToScreen is the callback passed to create a Screen Element

type RetortConfiguration

type RetortConfiguration struct {
	// UseSimulationScreen to output to a simulated screen
	// this is useful for automated testing
	UseSimulationScreen bool

	// UseDebugger to show a d overlay with output from
	// the retort.dev/d#Log function
	UseDebugger bool

	// DisableMouse to prevent Mouse Events from being created
	DisableMouse bool
}

RetortConfiguration allows you to enable features your app may want to use

type SetState

type SetState = func(a Action)

type State

type State []interface{}

State is local to a component. It is mutable via the setState function from UseState. Don't edit State directly, as retort will not know that you have, and will not trigger an update and re-render. It can be used to create new props to pass down to other components.

func UseContext

func UseContext(c *Context) State

UseContext lets you subscribe to changes of Context without nesting.

func (State) GetState

func (state State) GetState(stateType interface{}) interface{}

GetState lets you retrieve the state of your passed in type from a UseState hook.

Because we cannot use generics this is the closest we can get. This is like Properties where the stateType type is a key to the struct in the slice of interfaces. As such, you can only have one of a given type in state.

Directories

Path Synopsis
internal
quadtree
Via https://github.com/JamesLMilner/quadtree-go Converted to ints for our use case
Via https://github.com/JamesLMilner/quadtree-go Converted to ints for our use case