Skip to content

Jakobeha/devolve-ui.js

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

97 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

License

logo

devolve-ui: super simple reactive graphics for browser and terminal

Disclaimer: this is an early project. It's subject to change and have bugs

Live demo | Clone to get started

devolve-ui is a super simple graphics library for canvas-based websites (games) and TUIs. A single devolve-ui app can be embedded in a website and run on the command line via node.

devolve-ui is JSX-based like React, but renders to canvas or terminal instead of DOM. It should not be used for traditional web pages.

Example:

// https://github.com/Jakobeha/devolve-ui-demos/src/readme.tsx
import { DevolveUI, useState, useInterval } from '@raycenity/devolve-ui'

const App = ({ name }) => {
  const counter = useState(0)
  useInterval(1000, () => {
    counter.v++
  })

  return (
    <zbox width='100%'>
      <vbox x={2} y={2} gap={1}>
        <zbox width='100%'>
          <hbox width='100%'>
            <text color='white'>Hello {name}</text>
            <text color='white' x='100%' anchorX={1}>{counter.v} seconds</text>
          </hbox>
          <color color='orange' />
        </zbox>
        <source src='dog.png' width='100%' />
      </vbox>
      <border style='single' color='orange' width='prev + 4' height='prev + 4'/>
    </zbox>
  )
}

new DevolveUI(App, { name: 'devolve-ui' }).show()

// Works in node or browser (with additional pixi.js script)
terminal iterm browser

Important setup information: if adding to an existing project, besides installing, you must add this to your tsconfig.json for JSX support:

{
  "include": [
    /* you probably have this */
    "src/**/*.ts",
    /* but make sure to also add this */
    "src/**/*.tsx"
  ],
  "compilerOptions": {
    /* ... */
    /** if using esbuild, otherwise "jsx": "react" */
    "jsx": "preserve",
    "jsxImportSource": "@raycenity/devolve-ui",
  }
}

Pro tip: If you debug TUIs in IntelliJ, you can see console outputs in a separate tab from the terminal output!

Installing (with pnpm)

# if you don't have pnpm installed, uncomment the next line
# curl -fsSL https://get.pnpm.io/install.sh | sh -
pnpm add @raycenity/devolve-ui

Repository info

devolve-ui is built using esbuild. The package manager used is pnpm. Linting is done by standard, however we use a slightly modified version removing some warnings (ts-standardx.mjs). Docs are generated by mkdocs. Feel free to submit issues / pull requests on the Github.

Features

Cross-platform

devolve-ui is cross-platform (isomorphic): a devolve-ui application may run in both web browsers and terminals (via node.js). When the application is run in the terminal, graphics are much simpler and certain effects and animations are removed, hence the name "devolve"-ui.

When a devolve-ui application is run in the web browser, it uses pixi.js for rendering.

Super simple

devolve-ui uses JSX and React-style components: you write your UI declaratively and use hooks (useState, useEffect, useLazy, useInput) for local state and side-effects. Your UI is literally a function which takes the global state, and returns a render of your application.

Unlike React, the lowercase JSX nodes (views) which devolve-ui uses are not HTML elements, they are:

  • hbox, vbox, zbox: Layout child views
    • hbox: Places children horizontally
    • vbox: Places children vertically
    • zbox: Places children on top of each other (no position offsets)
  • text: Contains text
  • solid: Renders a solid color
  • border: Renders a border
  • source: Renders an image, video, or other external graphic
  • (WIP unstable) pixi: Can only be created via PixiComponent. These contain custom pixi components in the browser, and are invisible in TUIs.

Another notable difference is the layout system. devolve-ui does not use CSS, instead all node bounds are calculated using only the parent and previous child. As a result, you must specify bounds much more explicitly. See the Implementation section for more.

State and contexts are also handled differently. Essentially, useState returns a proxy instead of a getter / setter array. The code in React:

const [value, setValue] = useState(0)
setValue(value + 5)

translates in devolve-ui to:

const value = useState(0)
value.v += 5 // or value.v = value.v + 5

See the State / Lenses and Contexts sections for more detail.

Concepts

Prompt-based GUI

Prompt-based GUI is a new-ish paradigm where your application interfaces with the UI via prompts. devolve-ui has built-in support for prompt-based GUI via the PromptDevolveUI class. Read this article for more.

State / Lenses

Instead of useState returning a getter/setter array ([value, setValue]), it returns a lens. You can get the value of the lens with lens.v, and set the value with lens.v = newValue.

The key advantage of lenses is that if the lens contains an object, you can get a lens to its property foo via lens.foo. For example:

const parentLens = useState({ foo: { bar: { baz: 0 } } })
parentLens = { foo: { bar: { baz: 5 } } }

is equivalent to

const parentLens = useState({ foo: { bar: { baz: 0 } } })
const childLens = parentLens.foo.bar.baz
childLens.v = 5

This is particularly useful when you pass the child lens to a child component, like so:

const Parent = () => {
  const lens = useState({foo: {bar: {baz: 'hello'}}})
  // This prints 'hello world' for 2 seconds,
  // then prints 'goodbye world'
  return (
      <vbox>
        <text>{lens.foo.bar.baz.v}</text>
        <Child lens={lens.foo.bar.baz}/>
      </vbox>
  )
}

const Child = ({lens}) => {
    useDelay(2000, () => {
        lens.v = 'goodbye'
    })
    return (
        <text>world</text>
    )
}

Contexts

Contexts allow you to pass props implicitly, similar to React contexts. However, contexts in devolve-ui work slightly different: they are hooks instead of components.

// const fooBarContext = createContext<FooBar>() in TypeScript
const fooBarContext = createContext()

const Parent = () => {
  fooBarContext.useProvide({ foo: 'bar' })
  return <box><Child /></box>
}

const Child = () => {
  const value = fooBarContext.useConsume()
  // value is { foo: 'bar' }
  return <text>{value.foo}</text>
}

There are also state contexts, which combine the functionality of contexts and states: a state context is a context which provides a state lens instead of a value. Children can mutate the state, and the mutation will affect other children who use the same provided context, but not children who use a different provided context.

// const fooBarContext = createStateContext<FooBar>() in TypeScript
const fooBarContext = createStateContext()

const Grandparent = () => {
  // In the first parent, value is { foo: 'bar' } for the first 5 seconds, and { foo: 'baz' } after
  // because MutatingChild sets it
  // In the second parent, value remains { foo: 'bar' } because that child doesn't set it
  return (
    <box>
      <Parent>
        <MutatingChild />
      </Parent>
      <Parent>
        <Child />
      </Parent>
    </box>
  )
}


const Parent = ({ children }) => {
  fooBarContext.useProvide({ foo: 'bar' })
  return <box>{children}</box>
}

const MutatingChild = () => {
  const value = fooBarContext.useConsume()
  // value is { foo: 'bar' } for the first 5 seconds, and { foo: 'baz' } after
  useDelay(5000, () => { value.foo.v = 'baz' })
  return <text>{value.foo.v}</text>
}

const Child = () => {
  const value = fooBarContext.useConsume()
  // value is { foo: 'bar' } forever
  return <text>{value.foo.v}</text>
}

Regular (non-state) contexts are called props contexts and are essentially implicit props passed from parents to their children. State contexts are essentially implicit state passed from parents to their children. See this article for more explanation.

Implementation

Source

Directory overview

  • core: The main code of devolve-ui
    • core/hooks: Built-in hooks
      • core/hooks/intrinsic: Hooks requiring package-private functions and support in VComponent
      • core/hooks/extra: Hooks that you could create from the intrinsic ones
    • core/vdom: The "DOM" in devolve-ui: nodes, attributes, and JSX.
  • renderer: Platform-specific rendering
  • prompt: Prompt-based GUI helpers.

Notable types

  • VView: Virtual "DOM" node, e.g. box, text, color. Immutable: a new view is created on change.
  • VComponent: Component. Manages state, effects, input, etc. and renders a VView
  • Bounds: A view's bounds depend on the parent and previous view: therefore Bounds are literally a function from parent and previous view properties to a BoundingBox. See src/core/vdom/bounds.ts

About

One UI for both terminal (TUI) and browser

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages