limn
    ↑↓ ChooseEnter OpenEsc Close
    GitHub

    Documentation/Contributing a component

    Contributing a component

    The steps to add a component, and the conventions the existing ones follow. crates/limn/src/progress.rs is the shortest complete example: read it alongside this page.

    1. Decide whether it is a component

    A component earns its place when it packages a decision that should be made once: a layout that is easy to get wrong, a set of states, a keyboard behavior. If it is only two elements in a row, leave it to the caller.

    Before writing anything new, check Components. Many requests are an option on something that exists.

    2. Write it

    Create crates/limn/src/<name>.rs.

    //! One line saying what this is for.
    
    use crate::{Component, ComponentPalette, Element};
    
    /// What the host owns, and what the component reports.
    pub struct Progress {
        fraction: f32,
        palette: ComponentPalette,
        width: Option<f32>,
    }
    
    impl Progress {
        pub fn new(fraction: f32, palette: ComponentPalette) -> Self { /* ... */ }
    
        pub fn width(mut self, width: f32) -> Self { /* ... */ }
    }
    
    impl Component for Progress {
        fn render(self) -> Element { /* ... */ }
    }

    Then declare it in crates/limn/src/lib.rs: a mod line and a pub use.

    Conventions

    RuleReason
    new takes what is required: an ID if interactive, the content, the palette last.Every component reads the same way.
    Everything optional is a builder method returning Self.Callers set only what they need.
    The component is controlled. It takes its value and reports actions; it stores nothing between frames.The window stays a function of the host’s state.
    Validate numbers in builders: ignore values that are not finite, clamp ranges. A profile struct passes each number through crate::page::px where it is used.A bad value from a caller must not reach a renderer.
    Derive every ID the component creates from the caller’s ID, such as format!("{id}:body").IDs must be unique across the window.
    Take colors from the palette, radii and durations from crate::tokens(). Never write a color, a radius or a duration as a literal.Themes and tokens must reach every component.
    A disabled state renders passive elements, not a button that ignores clicks.It must be unreachable by keyboard as well as pointer.
    One name for one idea: ink for the color of the content, font_size for the size of text, width and height in logical pixels, size(ControlSize) for a control’s size, sizing(<Component>Sizing) for a density profile, accessibility_label for a name.A caller who knows one component can guess the next.
    A name that changes keeps its old form, marked #[deprecated] with the new name in its note.An application built on an earlier version still compiles, and is told what to change.
    What can be disabled has both disabled() and enabled(bool).The first reads well for a fixed case, the second takes a state the host computes; a caller should not have to remember which one a component has.

    | Everything public has a doc comment that says what the name does not: the unit, what a value is clamped to, what it makes the component report. That is every function, type, trait and constant, and every public field and variant. The crates carry #![warn(missing_docs)], so one without fails the build in CI. | The reference is read without the source beside it. |

    crates/limn/tests/conventions.rs holds the components to the rules that can be checked across all of them: both spellings of disabling and sizes that are not numbers. A component that joins a family joins its row there.

    Pills and circles are the exception to the radius rule: a radius of half the height is a shape, not a token.

    Choosing tokens

    ForUse
    A controltokens().radius.control
    A floating surfacetokens().radius.surface
    A group set into the pagetokens().radius.card
    A large containertokens().radius.panel
    A hover or press filltokens().motion.hover_ms
    Something arriving or leavingtokens().motion.enter_ms, exit_ms
    Something moving in placetokens().motion.move_ms

    See Animation before adding movement.

    3. Test it

    Add a #[cfg(test)] module in the same file. Cover at least:

    • the geometry or paint that defines the component;
    • every action it reports, by pointer;
    • bad input to each numeric builder;
    • a narrow width, if it contains text.

    A control that reports an action when it is activated also joins the table in crates/limn/tests/controls.rs, which holds every one of them to the same facts: one action for a click, for Enter and for Space; reached by Tab; and, disabled, inert, in the same place and announced as disabled.

    See Testing.

    Every component has a page of its own in the gallery. Write its sections in a module of crates/limn-gallery/src/pages/, one section for each aspect: variants, sizes, states. Show each state, not one happy example: disabled, loading, empty, long text. Then list the component in PAGES, with the titles of the sections that are its own. See Gallery and hot reload.

    Then look at it, in both themes and with square corners. Tests do not catch a label that sits two pixels low.

    Marking what is new

    Every page in PAGES says the version its component first shipped in, as since: "0.1.41": the version in Cargo.toml when it is added. The site marks the component “New” wherever its name is listed (the menu, the index, its own page, the search) while that version or the one after it is current, and stops by itself after that. Nothing else has to be set or removed.

    5. Document it

    Add a row to the right table in Components, and a doc comment on the struct and on every public method.

    Then write its guide, docs/components/<id>.md, named after its page in the gallery. The site shows the guide under the component running, so the two are read together. Every guide follows this outline, in this order, omitting what does not apply after Usage:

    # ComponentName
    
    One or two sentences: what it is for and when to use it.
    
    ## Import
    The `use` line a file needs.
    
    ## Usage
    The smallest complete use: the view, and the action handled.
    
    ## One heading for each thing it can do
    Variants, sizes, icons, steps: a snippet and what it changes.
    
    ## Keyboard
    | Key | Effect |
    
    ## Motion
    What animates, and with which duration token.
    
    ## Actions
    | Action | When |

    A test in the gallery checks that a guide belongs to a page, starts with the component’s name and has Import and Usage. Snippets use palette and state as a reader’s own view would have them; they are fragments, not programs. Button, Slider and Switch are the models.

    Writing style for these pages

    • English, present tense, second person for instructions.
    • One idea per sentence. Say what something does before how.
    • Tables for anything with more than two parallel items.
    • Every code sample must compile against the current API. Prefer pointing to a file that is built by Cargo.
    • State limits plainly, in a section of their own.

    Checklist

    • Module, mod and pub use added.
    • No literal colors, radii or durations.
    • Tests pass: cargo test -p limn -p limn-egui -p limn-gallery.
    • Shown in the gallery, checked in both themes and with square corners.
    • Row in docs/components.md; doc comments on the public API.