notesy::view

What a panel, a tab or a sidebar section shows is a view: plain values naming notesy's own widgets, which notesy draws. A script never draws; it describes, and a plugin's UI always looks like notesy, in any theme.

A view is a tree of parts. Each builder below returns an object (#{ kind: "row", icon: "check", text: "Call Ana" }); set any of its other fields after, like row.target = "call". A list is a column; plain text is a note.

rustuse notesy::{view, Color, Icon};

fn build(note) {
    let row = view::row(Icon::ArrowRight, "Record the demo video");
    row.target = "video";
    row.color = Color::Accent;
    view::column([
        view::header(Icon::Check, "Tasks", "3 left in this note"),
        view::stats([["3", "to do"], ["2", "done"]]),
        view::progress(2, 5),
        view::caption("to do"),
        row,
    ])
}

#Parts

Builder Other fields What it is
view::column(parts) parts one under another
view::columns(parts) weights, gap, min, max parts side by side, each as wide as its share of weights ([2, 1]: two thirds and one; equal when it has none), gap points apart (12); when there isn't room for each to be min points wide (160), as many to a row as fit, like CSS's flex-wrap. With max, the row is at most that wide, kept to the left: a toolbar in a wide tab. A part that's a list is a column in it
view::caption(text) a small uppercase heading
view::note(text) color, icon wrapped text, faint unless it has a color
view::header(icon, title, subtitle) tint an icon on a tinted tile, a title, a line under it
view::fact(icon, name, value) a name on the left, its value on the right
view::stats(items) tiles of a big value over a label: [[value, label], …] (at most 6)
view::chips(list) target chips in rows; #text ones in the tags' colors
view::row(icon, text) hint, target, command, faint, color, actions a row, like a link in the Links panel; actions as a fold's, over its right end
view::block(icon, title, lines) subtitle, target a soft block with lines on a bar, like a mention
view::progress(done, total) color a bar filled to done / total
view::button(label) icon, target, command, tone a button; tone is plain, soft, primary, danger or tag
view::empty(icon, title, body) action, target what shows where there's nothing yet, with a button when it has an action
view::pill(text) tone a small filled label: a state, a count; tone as a button's
view::sparkline(values) unit a line of numbers across, from nothing at its foot to the largest at its top, which is written at the top right with its unit; at most 2,000
view::tiles(items) small tiles side by side, each #{ label, icon, value, detail, color } (color is its icon's): the hours of a forecast, say. Past the panel's edge the strip scrolls sideways; at most 48
view::fold(title, parts) icon, hint, color, open, actions, key a heading with a chevron that opens and closes what's under it, an icon (in color) before its title and a hint at its right; open unless open is false. Open or closed, it stays as the user leaves it while its view is built again, kept by its key (its title unless it has one: give two with the same title a key each). actions, up to 4 #{ icon, target, tip, lit }, are small buttons at its right while the pointer's on its line, each heard as a click on its target; a lit one shows all the time, in the accent (which of several is chosen)
view::bars(values) labels, unit, max, color a bar a value on a scale from nothing to max (the largest, unless it says), as many of labels under them as fit, and the value under the pointer (with its label) at the top: the hours' chance of rain, say. At most 400
view::metrics(items) small cards in a grid, as many to a row as fit, each #{ label, icon, value, detail, color }: an icon and a label over a big value and a line under it. At most 48
view::range(label, low, high) min, max, mark, unit, icon, hint, color a span from low to high on a bar from min to max (the span itself unless it says): the label, a hint and an icon before it, its ends written beside it with their unit, mark a dot on the bar. A day of a forecast, say
view::lanes(lanes) target, adds lanes of cards side by side, like a kanban board, scrolling sideways when there are more than fit: lanes is [#{ title, cards: [#{ text, detail, done }] }] (at most 12 lanes, 500 cards in one; a lane or card without its title or text keeps its place, empty). A card is dragged within its lane or into another, its box ticks it, and with adds (the hint) a field at each lane's foot adds one. Each is heard at target as an object, by places counted from 0: #{ action: "move", lane, card, to_lane, to_card } (to_card counted with it taken out), #{ action: "tick", lane, card }, #{ action: "click", lane, card } or #{ action: "add", lane, text }. Nothing moves until the script builds it again saying so
view::markdown(text) markdown, drawn as notesy draws a note (headings, emphasis, code, links and tags, lists and tasks, quotes, code blocks, rules, columns), read-only; at most 20,000 characters
view::code(text) language, numbers code, read-only, in a well: in language's colors as a code block's fence names it (rust, py, sh, a plugin's own from [[languages]]), plain without one; its lines numbered unless numbers is false; a line wider than the well moved along with a sideways wheel (or Shift and the wheel) or a drag. At most 256 KB. Its colors come a moment after it first shows, and are kept: drawn again, it costs nothing
view::divider() a thin line across
view::space(points) room, up to 64 points

Icons, colors and tones are enums of notesy's names (Names: Icon, Color, Tone, Side, Method, Command, Zone, Event): Icon::Check, Color::Accent, Tone::Primary. Its own icons are by their names: the plugin's own [[icons]] (plugin.toml), notesy's (check, clock, file, folder, link, tag, calendar, bulb, alert, info, arrow-right, arrow-up, pin, plugin, canvas …), or another plugin's as <plugin id>.<name>. Colors are a theme color's key (accent, success, warning, danger, text_muted …), one of the plugin's own [[colors]], or #rrggbb.

#Clicks

A row, block, button or empty state with a target tells the script when it's clicked: the panel or tab's on_click handler gets the target (and a chip's target gets target:chip). One with a command runs that command instead, notesy's own or a plugin's. See notesy::views.

#Inputs

Parts the user changes. Each needs a target to be heard: when it changes, the panel or tab's on_change handler gets the target, the new value, and the note's path (or None), and then notesy builds the view again, so what it shows comes from the script (notesy::views).

Builder Other fields What it is What on_change gets
view::switch(label, on) hint, icon, target a line with a switch at its right, like a setting true or false
view::check(text, done) target, actions a line with a box to tick, like a task; ticked, its text is struck through. actions as a fold's, at its right true or false
view::input(hint) target, suggest, secret a field to type into; Enter sends what's in it and empties it. With suggest: true, its script suggests as it's typed in (below). With secret: true, it's masked as a password is, offers no suggestions, and no plugin hears the keys typed in it the text, or a suggestion's value
view::dropdown(options, selected) target a dropdown of options, one picked the option picked
view::segmented(options, selected) target up to five short options side by side, one picked the option picked

It's dropdown and not select because select is one of Rune's own words: a function by that name can be offered but never called.

selected is the one picked: by what it's worth, then by its name, then by its place from 0.

options is a list. An option is its name alone, or a table when it has more to say than that: value is what target hears when it's picked (its name, unless it's given one of its own), subline is a second line under its name, detail is faint text at its right, and icon is an icon before it, notesy's or the plugin's own. An option with a subline is given a taller row; the rest stay as they are. A segmented choice has room for neither subline nor detail and leaves both out.

rustuse notesy::{store, view, views};

pub fn ready() {
    views::panel("spans", "Spans", |note| {
        let spans = [
            #{ value: "day", label: "Day", subline: "Since midnight", detail: "24 hours", icon: "clock" },
            #{ value: "week", label: "Week", subline: "From Monday", detail: "7 days", icon: "calendar" },
        ];
        let span = view::dropdown(spans, store::get("span").unwrap_or("week"));
        span.target = "span";
        view::column([span])
    });
}

Written as plain text (["Day", "Week"]), an option is its own name and its own value, which is what every dropdown did before there was more to say.

rustuse notesy::{store, view, views};

pub fn ready() {
    let tasks = views::panel("tasks", "Tasks", |note| {
        let hide = view::switch("Hide done tasks", store::get("hide").unwrap_or(false));
        hide.target = "hide";
        let span = view::segmented(["Today", "This week"], store::get("span").unwrap_or("Today"));
        span.target = "span";
        view::column([hide, span])
    });
    tasks.on_change(|target, value, note| store::set(target, value));
}

#Suggestions

A field with suggest: true asks its script what to suggest as it's typed in. Once typing pauses (a quarter of a second), the panel or tab's on_suggest handler gets the target, the text and the note's path, and answers with the view's suggest(target, text, items): then, or later, once it knows. So what it suggests can come from anywhere it may reach: its store, the vault, a site its manifest names (notesy::net).

Each item is text, or #{ text, detail, icon, value, view }. They show under the field, to pick with ↑, ↓ and Enter or a click; Esc puts them away. One with a view (any part of a view: a column of a title and a line under it, a row with a pill) shows that instead of its icon, text and detail, lit and picked as the rest are; what's in it is only looked at. A pick reaches on_change with the item's value (its text, unless it has one); Enter with none lit sends what was typed, as a field without suggestions does. Only the answer for what was last typed shows, so one that comes back late is left out. At most 50 show.

rustuse notesy::{json, net, store, view, views};

pub fn ready() {
    let app = #{ panel: () };
    app.panel = views::panel("places", "Places", |note| {
        let add = view::input("Add a place");
        add.target = "add";
        add.suggest = true;
        view::column([add])
    });
    app.panel.on_suggest(|target, text, note| {
        net::fetch(`https://geocoding-api.open-meteo.com/v1/search?name=${net::encode(text)}`, |result| {
            let items = [];
            if let Ok(response) = result {
                if let Ok(answer) = json::parse(response.text) {
                    for r in answer.get("results").unwrap_or([]) {
                        items.push(#{ text: r.name, detail: r.country, icon: "pin", value: `${r.name}, ${r.country}` });
                    }
                }
            }
            app.panel.suggest(target, text, items);
        });
    });
    // What was picked (its value), or typed and sent with Enter.
    app.panel.on_change(|target, value, note| store::set("place", value));
}

#Coming in

Any part can come in as it first shows: set its enter to how. notesy plays it as its Motion setting says (with motion off, parts are simply there): when the part first shows, as the panel opens or a new row turns up, and again after it's been out of sight a while. Building the view again doesn't play it again.

enter How it comes in
"fade" fading in where it is
"rise" fading in as it rises a little into place
"slide" fading in from a little to its left
"pop" fading in as it grows into place, a touch past and back

For more say, enter is an object, #{ kind: "rise", delay: 0.2, span: 0.4, ease: "back" }: delay seconds before it starts (up to 5), over span seconds (0.3 unless it says; up to 2), eased by name as a drawing's keyframes are (notesy::icons). A column with a stagger brings its parts in one after another, each that many seconds after the one before (up to 1): a list settling in. A stagger on its own rises.

rustuse notesy::{view, Icon};

fn build(note) {
    let rows = [];
    for task in ["Call Ana", "Book the train", "Pack for Eryri"] {
        rows.push(view::row(Icon::Check, task));
    }
    let head = view::header(Icon::Check, "Tasks", "3 left in this note");
    head.enter = "fade";
    let list = view::column(rows);
    list.enter = "rise";
    list.stagger = 0.04;
    view::column([head, list])
}

#When something's wrong

A part that's wrong (no text on a row, a kind there isn't) shows as a red line where it was, saying what's wrong, and the rest of the view still shows. A view has at most 2,000 parts, nested at most 16 deep.

Every page