Guide
Widgets
tui4s-widgets, by family, as the gallery example shows them.
Gallery.scala
/** The containers: a panel, a split pane, an accordion and tabs. */
def containers(using Frame): UI < Sync =
for
split <- SplitPane(
Panel(title = "Left")(span("files")),
Panel(title = "Right")(span("preview")),
)(size = Model.Local(40))
acc <- Accordion(
Accordion.Section("General", span("Name and email.")),
Accordion.Section("Advanced", span("Cache and logs.")),
)()
tabs <- Tabs("One", "Two", "Three")(
panel = i => span(s"The panel of tab ${i + 1}."),
).ui
yield div.style(_.gap(1.px))(
Panel(title = "Panel")(span("A border style and a title on it.")),
labeled("SplitPane (Tab to the divider, then ←/→)", split),
labeled("Accordion", acc),
labeled("Tabs", tabs),
)
The gallery example (examples/target/bin/gallery) shows each widget with live state. The arrows (or F1 to F5) pick a family. Tab goes from widget to widget. Ctrl-P opens the command palette, and Ctrl-Q quits.
build.sbt
libraryDependencies += "io.github.yuri4n" %% "tui4s-widgets" % "<version>"
// a Scala.js or a cross-built project:
libraryDependencies += "io.github.yuri4n" %%% "tui4s-widgets" % "<version>"
The rules of each widget
- A
Modelsays who owns the value.Model.Local(initial): the widget owns it.Model.Bound(ref): the parent owns aSignalRef, and the widget writes it beforeonChange.Model.Controlled(signal): the parent owns a signal, andonChangeasks; the parent accepts or rejects. - A widget that only builds a UI is a
UI. A widget that allocates state isUI < Sync. A widget that starts a fiber isUI < (Sync & Scope). Build a widget once, where its state lives. - The Scaladoc of each widget that takes the focus has its keys. A layer (a modal, a menu, the palette) has
focusTrap,focusAutoandfocusRestore. Esc closes it. - The colours are the
--tui4s-*custom properties. GiveColors.defaultsto the run (override def stylesheet = Colors.defaults). With no value, a widget keeps its marks (›,[x], bold text) and shows no colour.
Data
The charts, ListView, DataTable, TreeView and Pager:
Gallery.scala
/** The data widgets: the charts, a list, a table, a tree and a pager. */
def data(using Frame): UI < (Sync & Scope) =
for
fruits <-
Signal.initRef(Chunk("apple", "banana", "cherry", "date", "elder"))
list <- ListView(fruits, identity[String], height = 3)(span(_))
table <- DataTable(
Signal.initConst(langs),
_.name,
DataTable.Column("Name", _.name, DataTable.Width.Fixed(8)),
DataTable.Column(
"Year",
_.year.toString,
order = Present(Ordering.by(_.year)),
),
DataTable.Column("Kind", _.kind, DataTable.Width.Fill),
)(height = 3)
tree <- TreeView(
Signal.initConst(
Chunk(
TreeView.Node(
"src",
Chunk(
TreeView.Node("Main.scala"),
TreeView.Node("Ui.scala"),
),
),
TreeView.Node("README.md"),
),
),
identity[String],
height = 4,
)(span(_))
log <- Pager(
Signal.initConst(
Chunk.from((1 to 40).map(i => s"event $i: served")),
),
height = 3,
)
spinner <- Spinner(label = "working")
yield div.style(_.gap(1.px))(
div.style(_.row.gap(2.px))(
Sparkline(Signal.initConst(Chunk(0.1, 0.4, 0.9, 0.6, 0.3))),
Gauge(Signal.initConst(0.42), width = 10),
ProgressBar(Signal.initConst(3), Signal.initConst(10), width = 10),
spinner,
),
BarChart(
Signal.initConst(Chunk("Mon" -> 3.0, "Tue" -> 7.5, "Wed" -> 5.0)),
width = 20,
),
labeled("ListView (↑/↓, PgUp/PgDn, Home/End)", list),
labeled("DataTable (1 to 3 sort)", table),
labeled("TreeView (→ opens, ← closes)", tree),
labeled("Pager (f follows, n/N next match)", log),
)
Input
SearchBox, Autocomplete, Checklist, RadioGroup, NumberStepper and DateField:
Gallery.scala
/** The input widgets: a search box, an autocomplete, a checklist, a radio
* group, a number stepper and a date field.
*/
def input(using Frame): UI < (Sync & Scope) =
for
search <- SearchBox()
auto <- Autocomplete(
Signal.initConst(langs.map(_.name)),
matcher = Autocomplete.Match.Fuzzy,
placeholder = "a language",
)
tasks <- Checklist(
Signal.initConst(
Chunk("write the widgets", "write the tests"),
),
identity[String],
height = 2,
)(span(_))
size <- RadioGroup("Small", "Medium", "Large")()
count <- NumberStepper(value = Model.Local(3), min = 0, max = 10)
date <- DateField()
yield div.style(_.gap(1.px))(
labeled(
"SearchBox",
div(search.view, span(search.query.map(q => s"query: $q"))),
),
labeled("Autocomplete (↓, Enter)", auto),
labeled("Checklist (Space)", tasks),
labeled("RadioGroup (↑/↓)", size),
labeled("NumberStepper (↑/↓, PgUp/PgDn)", count),
labeled("DateField", date),
)
Feedback
StatusBar, KeyHints, CommandPalette, Modal.confirm, Modal.prompt and Toasts. The app builds the layers once, and its root holds their views:
Gallery.scala
/** The feedback widgets: a status bar, key hints, the command palette, a
* confirm, a prompt and toasts.
*/
def feedback(layers: Layers)(using Frame): UI < Sync =
for
answer <- Signal.initRef("no answer yet")
name <- Signal.initRef("notes.txt")
yield div.style(_.gap(1.px))(
labeled(
"StatusBar",
StatusBar(
Signal.initConst("gallery.scala 120 lines"),
right = span("UTF-8"),
),
),
labeled(
"KeyHints",
KeyHints.of(Chunk("↑/k" -> "up", "↓/j" -> "down", "Enter" -> "open")),
),
labeled(
"CommandPalette",
div.style(_.row)(
button("Open the palette (Ctrl-P)").onClick(layers.palette.open),
),
),
labeled(
"Modal.confirm and Modal.prompt",
div.style(_.row.gap(2.px))(
button("Delete").onClick(
layers.confirm.ask("Delete the file?")(yes =>
answer.set(if yes then "deleted" else "kept"),
),
),
span(answer),
button("Rename").onClick(
layers.prompt.ask("New name?")(n => n.fold(Kyo.unit)(name.set)),
),
span(name.map(n => s"name: $n")),
),
),
labeled(
"Toasts",
div.style(_.row)(
button("Show a toast").onClick(
layers.toasts.show("Saved", Toasts.Level.Success),
),
),
),
)
Navigation
Menu.bar, Tooltip, Breadcrumbs, Pagination, Tags and Wizard:
Gallery.scala
/** The navigation widgets: a menu bar, a tooltip, breadcrumbs, pagination,
* tags and a wizard.
*/
def navigation(using Frame): UI < Sync =
for
done <- Signal.initRef("nothing chosen")
file = Keymap.of(
Binding.of("New", Chord.ctrl('n'))(done.set("File › New")),
Binding.of("Open", Chord.ctrl('o'))(done.set("File › Open")),
)
help =
Keymap.of(
Binding.of("About", Chord(Keyboard.F12))(done.set("Help › About")),
)
menu <- Menu.bar(Menu.Group("File", file), Menu.Group("Help", help))
tip <- Tooltip("Saves the file")(button("Save"))
path <- Signal.initRef(Chunk("home", "docs", "widgets"))
crumb <-
Breadcrumbs(path, onPick = i => path.updateAndGet(_.take(i + 1)))
page <- Signal.initRef(0)
pages <- Pagination(Signal.initConst(12), page = Model.Bound(page))
tags <- Tags(tags = Model.Local(Chunk("scala", "kyo")))
steps <- Wizard(
Wizard.Step("Account", span("Pick a user name.")),
Wizard.Step("Profile", span("Add a photo.")),
Wizard.Step("Done", span("All set.")),
)(onFinish = done.set("wizard finished"))
yield div.style(_.gap(1.px))(
labeled("Menu (↓ opens, Enter runs)", div(menu, span(done))),
labeled("Tooltip (on focus)", tip),
labeled("Breadcrumbs", crumb),
labeled(
"Pagination (←/→)",
div(pages, span(page.map(p => s"page ${p + 1} of 12"))),
),
labeled("Tags (Enter adds, Delete removes)", tags),
labeled("Wizard", steps),
)