Data Display Components

Tooltips, popovers, charts, tables and trees present information around the application's content. Install cn_bundle() as described in Getting Started. Examples use these imports and the application builder's ctx:

#![allow(unused)]
fn main() {
use blinc::cn::prelude::*;
use blinc::layout::binding::IntoReactive;
use blinc::layout::prelude::{FontWeight, Text, div, text};
}

Tooltip

Pass a trigger builder and supply its text:

#![allow(unused)]
fn main() {
tooltip(|| div().child(button("Hover me")))
    .text("More information about this action")
    .side(TooltipSide::Top)
}

TooltipSide has Top (the default), Bottom, Left and Right. .align(..), .offset(..), .open_delay_ms(..) and .close_delay_ms(..) control placement and timing. The text accepts a Signal or Computed:

#![allow(unused)]
fn main() {
let hint = ctx.use_signal("Ready to upload".to_string());

tooltip(|| div().child(button("Upload"))).text(hint)
}

The popup uses .cn-tooltip and a side class such as .cn-tooltip--top. Its look comes from CSS. There is no configurable arrow part.

Hover Card

A hover card takes trigger and content builders returning Div:

#![allow(unused)]
fn main() {
hover_card(|| div().child(text("@jane")))
    .content(|| {
        div().flex_row().gap(3.0)
            .child(avatar().fallback("JD").size(AvatarSize::Large))
            .child(div().flex_col().gap(1.0)
                .child(text("Jane Doe").weight(FontWeight::Bold))
                .child(muted("Software developer")))
    })
    .side(HoverCardSide::Bottom)
    .align(HoverCardAlign::Start)
}

Its popup is .cn-hover-card-content. .open_delay_ms(..) and .close_delay_ms(..) tune the interaction delays.

Popover

A popover opens on click. Its trigger builder receives the current open value; the component manages toggling and dismissal:

#![allow(unused)]
fn main() {
let notifications = ctx.use_signal(true);

popover(|_open| div().child(button("Preferences")))
    .content(move || {
        div().flex_col().gap(3.0)
            .child(text("Notifications"))
            .child(switch(notifications).label("Email notifications"))
    })
    .side(PopoverSide::Bottom)
    .align(PopoverAlign::Start)
    .offset(8.0)
}

The content builder returns a Div. Runtime values inside it bind to Signals; there is no public .open(signal) or .on_open_change(...) builder. .class(..) targets the trigger wrapper. The separately mounted panel is .cn-popover-content, so use that class to restyle the panel rather than a descendant selector rooted at the trigger.

Chart

Line, area and bar charts draw on a canvas, with the value axis, category labels, legend and tooltip as ordinary elements. Moving the pointer across a chart marks the category under it (a line through its points, a band behind its bars) and shows each series' value there.

#![allow(unused)]
fn main() {
use blinc::cn::prelude::*;

cn::line_chart()
    .series("CPU", &[30.0, 45.0, 40.0, 60.0, 55.0])
    .series("Memory", &[20.0, 25.0, 30.0, 35.0, 40.0])
    .labels(vec!["Mon".into(), "Tue".into(), "Wed".into(), "Thu".into(), "Fri".into()])
    .format(|v| format!("{v}%"))
    .dots()
}

cn::area_chart() fills down to the axis; .stacked() piles the series, each filling down to the one below. cn::bar_chart() sets the series side by side in each category, or piles them when .stacked(); .horizontal() lays the bars along the width with the categories down the side. Bars are rounded at the end away from the axis.

#![allow(unused)]
fn main() {
cn::bar_chart()
    .series("Desktop", &[186.0, 305.0, 237.0])
    .series("Mobile", &[80.0, 200.0, 120.0])
    .labels(vec!["Jan".into(), "Feb".into(), "Mar".into()])
}

.curve(ChartCurve::Linear) or Step replace the smooth curve, .height(..) and .width(..) size it (it fills its parent's width unless set), .min(..) and .max(..) fix the value axis, and .no_grid(), .no_axes(), .no_tooltip() and .legend(false) leave parts out.

Pie and donut

cn::pie_chart() sizes slices by their share of the total, clockwise from the top; they sweep in, and ease to new values. .donut(0.6) cuts a hole of that share of the radius, and .total("Visitors") writes the total in it with the caption under. Hovering pulls a slice out and shows its value and share.

#![allow(unused)]
fn main() {
cn::pie_chart()
    .slice("Chrome", 275.0)
    .slice("Safari", 200.0)
    .slice("Other", 90.0)
    .donut(0.6)
    .total("Visitors")
}

.data(..) takes a list, Signal or Computed of PieSlice; .size(..) sets the diameter (220 unless set). The total is .cn-chart-pie-total and its caption .cn-chart-pie-caption.

Live data

.data(..) takes all the series as a list, Signal or Computed of ChartSeries, and .labels(..) likewise. The chart follows them in place, easing from the values it shows to the new ones:

#![allow(unused)]
fn main() {
let visitors = ctx.use_signal(vec![
    ChartSeries::new("Desktop", vec![186.0, 305.0, 237.0]),
    ChartSeries::new("Mobile", vec![80.0, 200.0, 120.0]),
]);

cn::area_chart().data(visitors).stacked()
}

Styling

Series take --chart-1 to --chart-5 in turn, from the theme (one palette per scheme); ChartSeries::color(..) takes any CSS colour for one. The grid, the pointer's cursor and the rings round dots are --cn-chart-grid, --cn-chart-cursor and --cn-chart-surface. Setting those variables restyles every chart:

:root { --chart-1: #16a34a; --cn-chart-grid: transparent; }

The parts are .cn-chart, .cn-chart-plot, .cn-chart-axis (a crowded label carries data-hidden), .cn-chart-legend with .cn-chart-legend-item and .cn-chart-swatch, and .cn-chart-tooltip with its -title, -row, -name and -value.

Sparklines, thresholds, histograms and comparisons

These are drawn on the same plot:

#![allow(unused)]
fn main() {
let frame_times = vec![12.0, 18.0, 15.0, 22.0];
let diffs = vec![1.0, 2.0, 2.5, 3.0, 5.0];

// A trend beside text: no axes, grid, legend or tooltip, 100 by 24.
cn::spark_line(vec![1.0, 2.5, 2.0, 3.5, 3.0]).color("var(--success)").filled();

// Values against budgets: each dot takes its band's colour.
cn::threshold_line_chart()
    .values(frame_times)
    .band(ChartBand::new(0.0, 16.67).label("60 fps"))
    .band(ChartBand::new(16.67, 33.33).label("30 fps"))
    .baseline(16.67);

// Values counted into equal-width bins.
cn::histogram(diffs).bins(30).log_scale().marker(5.0);

// Baseline against current per category, the change written beside it.
cn::comparison_bar_chart()
    .item("Render", 12.5, 14.2)
    .item("Layout", 3.2, 3.0)
    .threshold(10.0)
}

Any chart takes .band(..) for a shaded value range (good, warning and bad in turn unless given a colour) and .marker(ChartMarker::new(v)) for a dashed line at a value. A band's label is .cn-chart-band-label; a comparison's notes are .cn-chart-note with data-tone="bad" past the threshold or "good" for a decrease.

Table

Rows of cells under a header, with an optional footer and caption:

#![allow(unused)]
fn main() {
let chosen = ctx.use_signal(false);

table()
    .w_full()
    .child(table_header().child(
        table_row().child(table_head("Invoice")).child(table_head("Amount")),
    ))
    .child(table_body()
        .child(table_row()
            .child(table_cell().child(text("INV001")))
            .child(table_cell().child(text("$250.00"))))
        .child(table_row()
            .selected(chosen)
            .child(table_cell().child(text("INV002")))
            .child(table_cell().child(text("$1,200.00")))))
    .child(table_caption("Recent invoices"))
}

.selected(..) takes a bool, Signal or Computed and follows it in place.

Styling

The look is the .cn-table* rules. .cn-table sets the text colour every cell inherits, the border and the radius; .cn-table-header and .cn-table-footer sit on --surface-overlay; .cn-table-head and .cn-table-caption text is muted. Body rows highlight on hover, and a selected row carries data-state="selected". The variables --cn-table-border, --cn-table-radius, --cn-table-row-hover and --cn-table-row-selected adjust them:

.cn-table-row[data-state="selected"] { background: color-mix(in srgb, var(--primary) 20%, transparent); }

Tree

tree_view() adds roots with .node(key, label, configure) and nested nodes with .child(key, label, configure):

#![allow(unused)]
fn main() {
let chosen = ctx.use_signal("main".to_string());

div().flex_col().gap(2.0)
    .child(tree_view()
        .node("src", "src", |n| {
            n.expanded()
                .child("main", "main.rs", |n| n.icon(to_svg(icons::FILE, 16.0)))
                .child("ui", "ui", |n| {
                    n.child("button", "button.rs", |n| n)
                })
        })
        .node("cargo", "Cargo.toml", |n| n)
        .selected("main")
        .on_select(move |key| chosen.set(key.to_string())))
    .child(Text::bound(chosen.into_reactive()))
}

.selected(..) sets an initial key. .on_select(..) reports subsequent selection; it does not bind the tree's selection to an external signal.

Clicking a row selects it and opens or closes it. Once the tree has been clicked, Up and Down walk the shown rows, Right opens a row or enters its children, Left closes it or goes to its parent, Home and End jump to the ends, and Enter or Space selects.

A node's .diff(TreeNodeDiff::Added) (or Removed, Modified) colours its row. .guides(false) hides the guide lines; .indent(px) sets the indent (14px by default).

Styling

Each .cn-tree-item holds a .cn-tree-row and, with children, a .cn-tree-group. A row carries data-selected, data-diff and, while the keys are on it, data-focus-visible. An item and its group carry data-state="open" or "closed". The other parts are .cn-tree-chevron, .cn-tree-icon, .cn-tree-label and .cn-tree-guide:

.cn-tree-row[data-selected] { background: var(--accent-subtle); }
.cn-tree-row[data-diff="added"] { color: #22c55e; }
.cn-tree[data-guides] .cn-tree-guide { background: var(--text-tertiary); }

Kbd

Keyboard shortcuts can combine several keycaps:

#![allow(unused)]
fn main() {
div().flex_row().items_center().gap(2.0)
    .child(text("Search"))
    .child(div().flex_row().gap(0.5)
        .child(kbd("⌘"))
        .child(kbd("K")))
}

The fill, border, radius, shadow and text colour come from .cn-kbd. --cn-kbd-bg and --cn-kbd-border recolour it.

Examples

User Profile Card

#![allow(unused)]
fn main() {
hover_card(|| {
    div().flex_row().items_center().gap(2.0)
        .child(avatar().fallback("JD").size(AvatarSize::Small))
        .child(text("Jane Doe"))
})
.content(|| {
    div().flex_col().gap(3.0)
        .child(text("Jane Doe").weight(FontWeight::Bold))
        .child(muted("Developer and designer"))
        .child(text("Building desktop tools with Blinc."))
        .child(div().flex_row().gap(4.0)
            .child(label("120 followers"))
            .child(label("42 following")))
})
}

Dashboard Chart

#![allow(unused)]
fn main() {
card()
    .child(card_header()
        .title("Revenue overview")
        .description("Monthly revenue"))
    .child(card_content().child(
        area_chart()
            .series("Revenue", &[1200.0, 1500.0, 1400.0])
            .labels(vec!["Jan".into(), "Feb".into(), "Mar".into()])
            .height(300.0)
            .format(|value| format!("${value:.0}")),
    ))
}