Ribir Documentation

repository·master·Indexed 23 days ago

https://github.com/ribirx/ribir

A non-intrusive, data-driven GUI framework for Rust that enables multi-platform application development for Linux, Windows, macOS, and Web. Ribir features a declarative macro-based DSL, granular reactivity without a Virtual DOM, and a comprehensive widget composition system. The ecosystem includes ribir-bot for PR and release automation, ribir-cli for WASM bundling and serving, and ribir_dev_helper for image-based testing.

Tokens
96.5K
Snippets
203
Records
475
Agent score
82%

What's inside Ribir

  1. What is Ribir?

    master

    Ribir is an open-source Rust framework designed for building native, multi-platform applications (desktop, mobile, web, and server-side rendering) from a single codebase.

    It uses a non-intrusive declarative programming model where the UI is treated as a re-description of data interaction. Instead of managing complex UI states or inheriting from base classes, developers focus on the data API, and the UI automatically responds to data modifications. This allows you to design your application logic and data structures independently of the UI implementation.

  2. Explore the Ribir Gallery

    master

    The Ribir Gallery is an interactive showroom and learning resource for the Ribir declarative GUI framework. It is organized into four sections to help developers progress from seeing results to understanding core engineering concepts:

    1. Showcase: Demonstrates complex, real-world applications (e.g., Wordle Game, Messages App, Pomodoro Timer) to highlight state management ($state), complex layouts, and performance.
    2. Widgets: An interactive dictionary of built-in UI components (Inputs, Data Display, Navigation, Layouts, etc.) based on Material Design principles. Each component includes interactive previews and copy-pasteable declarative code snippets.
    3. Animations: Showcases Ribir's zero-boilerplate approach to animations, including transitions, state-driven motion, and easing curves using minimal Transition configurations.
    4. Concepts: Deep dives into Ribir's core pillars, such as reactivity (granular updates without a Virtual DOM), 'Fat Objects' (applying styles like margin or radius directly), and the fn_widget! declarative macro.
  3. What is FatObj and how does it work?

    master

    FatObj<T> is a generic wrapper used during the build phase to attach universal built-in attributes (like margin, background, or on_tap) to any Widget. This prevents individual widgets from having to implement these features manually.

    Key Mechanisms

    1. Lazy Initialization: FatObj maintains the state for all built-in attributes but defaults them to empty. An attribute's related widget is only initialized if it is explicitly used, ensuring no performance overhead for unused features.
    2. Composition: During the final construction stage, FatObj composes the wrapped Widget with the enabled built-in features into a single Widget tree. For example, applying a margin to a Text widget results in a structure like: Margin(MixBuiltin(Text)).
  4. What is a widget in Ribir?

    master

    In Ribir, a widget is the fundamental unit describing a view (e.g., buttons, text boxes, lists, or the entire app). In code, a widget can be a function, a closure, or a data object.

    Ribir categorizes widgets into four types:

    1. function widget: A function or closure returning a Widget.
    2. Compose widget: Used for building UI through composition.
    3. Render widget: (Advanced) For low-level rendering.
    4. ComposeChild widget: (Advanced) For specialized child composition.

    Note on terminology:

    • widget (lowercase): A generic term for any UI unit.
    • Widget (capitalized): A specific type that serves as the entry point for all widgets into the view.
  5. Configure SingleChild and MultiChild widget types

    master

    Ribir uses traits to enforce how many children a widget can accept. You can specify this behavior by deriving SingleChild or MultiChild on your widget struct.

    • SingleChild: For widgets that accept exactly one child (e.g., Padding, SizedBox, Container).
    • MultiChild: For widgets that accept a list of children (e.g., Row, Column, Stack).
    #[derive(SingleChild, Declare)]
    pub struct Container;
    
    #[derive(MultiChild, Declare)]
    pub struct Row;
  6. How Variant (Flexible Data Consumption) works

    master

    Variant is a wrapper around the Provider system that unifies the API for consuming both static values and dynamic/reactive states.

    Instead of choosing between Provider::of (for static) and Provider::state_of (for reactive), you use Variant. It automatically detects if a provider is a watcher provider (reactive) or a value provider (static).

    Key Advantages

    • Unified API: Use Variant::new regardless of the underlying data type.
    • Automatic Reactivity: If the provider is a watcher, the widget automatically updates when the value changes.
    • Easy Mapping: Use .map() to transform values while preserving reactivity.
    • Built-in Fallbacks: Use new_or(), new_or_default(), or new_or_else() to handle missing providers.
    // Automatically gets Color from provider (static or reactive)
    let color = Variant::<Color>::new(BuildCtx::get()).unwrap();
    
    @Container {
        size: Size::new(100., 100.),
        background: color,
    }
  7. Compose child widgets with `with_child`

    master

    In Ribir, composition is achieved using the with_child method. This is the underlying mechanism for the @ DSL syntax. Even properties that seem like simple values (like the text in a Button) are often implemented as child widgets to allow for flexible composition (e.g., switching between a text button and an icon button without unnecessary memory allocation).

    use ribir::prelude::*;
    
    // Composing a text button
    let text_btn = Button::declarer()
      .finish()
      .with_child("Text Button");
    
    // Composing an icon button
    let icon_btn = Button::declarer()
      .finish()
      .with_child(Icon.with_child(svg_registry::get_or_default("search")));
  8. Listen to expression changes with `watch!`

    master

    The watch! macro creates a subscribable rxRust stream that monitors all $-marked states in an expression.

    Difference between pipe! and watch!:

    • pipe! = (initial value + rxRust stream). Use this to initialize widget properties.
    • watch! = rxRust stream only. Use this when you need to manually subscribe to changes to perform side effects (like manual state writes).

    Lifecycle Management: When you call .subscribe() on a watch! macro, you must manage the subscription lifecycle:

    1. Manual Unsubscribe: If the subscription's lifecycle is shorter than the state it listens to (e.g., using external state in a widget), call .unsubscribe() in the widget's on_disposed handler.
    2. Circular References: If the downstream of a watch! subscription performs a write operation on the state being watched, you must manually call .unsubscribe() to prevent memory leaks caused by circular references.
    // Example: Manual subscription with lifecycle management
    fn show_name(name: Stateful<String>) -> Widget<'static> {
      fn_widget!{
        let mut text = @Text { text: "Hi, Guest!" };
        let u = watch!($read(name).to_string()).subscribe(move |name| {
          $write(text).text = format!("Hi, {}!", name).into();
        });
    
        // Unsubscribe when the widget is destroyed to avoid leaks
        @(text) { on_disposed: move |_| u.unsubscribe() }
      }
      .into_widget()
    }
  9. How the Ribir layout system works

    master

    Ribir uses a "Constraints Down, Size Up" single-pass layout model. This process follows three core principles:

    1. Constraints Down: Parent Widgets pass BoxClamp constraints to child Widgets, defining the minimum and maximum width and height the child can occupy.
    2. Size Up: Child Widgets calculate their own size based on those constraints and return a Size to the parent.
    3. Parent Sets Position: The parent uses the child's returned size to determine its position within the parent's coordinate system.

    This model ensures efficient and flexible UI layout by decoupling size calculation from positioning.

  10. Manage interactive state with `Stateful` and `$`

    master

    To make widgets interactive, you must use State, which makes data watchable and shareable.

    The State Lifecycle

    1. Initialize: Convert data into state using Stateful::new(value).
    2. Map: Declare how state maps to the view.
    3. Interact: Modify data through state (e.g., in event handlers).
    4. Update: Ribir automatically updates the view when state changes.

    $ Syntactic Sugar

    • $read(state): Returns a read reference to the state.
    • $write(state): Returns a write reference to the state.

    When used inside a move closure, $read and $write automatically handle state cloning (e.g., $write(count) expands to using a clone_writer()), allowing for easy state sharing across event handlers.

    use ribir::prelude::*;
    
    fn main() {
      App::run(fn_widget! {
        // 1. Initialize state
        let count = Stateful::new(0);
        
        @Button {
          // 2. Modify state on interaction
          on_tap: move |_| *$write(count) += 1,
          
          // 3. Map state to view (using pipe! for transformations)
          @ pipe!($read(count).to_string())
        }
      });
    }
  11. Understanding widget events: on_change vs on_submit

    master

    Ribir distinguishes between live interaction and finalized data through two primary event types:

    | Event | Trigger | Behavior | | :--- | :--- | : | | on_change | User interaction (drag, type, click) | Fires frequently (every tick/keystroke). Represents live intent. | | on_submit | Explicit commit (Enter, Blur) | Fires once on completion. Represents finalized data. |

    Choosing the right event for your widget category

    Immediate Feedback Widgets (e.g., Slider, Checkbox, Switch, Tabs):

    • These widgets have meaning at every intermediate step.
    • Use on_change as the primary event.

    Submit-Confirm Widgets (e.g., Input, TextArea):

    • The value is often in-progress until committed.
    • Use on_submit for committing finalized data (e.g., form submission, search query).
    • Use on_change for real-time feedback (e.g., live validation, search-as-you-type, password strength).
  12. Understand Ribir Widget categories and composition

    master

    In Ribir, everything is a Widget. You build user interfaces by composing different Widgets using the fn_widget! macro and @ syntax. Widgets fall into two main categories:

    1. Compose Widgets: Used to build UI by nesting other widgets (e.g., Button, List).
    2. Render Widgets: Responsible for specific drawing or layout logic (e.g., Text, Container).

    To create a widget, use the fn_widget! macro and call .into_widget() on the result.