Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

App

App is initialized with an initial state, display size, and a view function. The view function takes a reference to your state and returns a view:

#![allow(unused)]
fn main() {
extern crate buoyant;
extern crate embedded_graphics;
use buoyant::app::App;
use buoyant::view::prelude::*;
use buoyant::primitives::Size;
use embedded_graphics::pixelcolor::Rgb888;

#[derive(Clone, Default)]
struct State {
    count: i32,
}

fn counter_view(state: &State) -> impl View<Rgb888, State> + use<> {
    let count = state.count;
    Button::new(
        |state: &mut State| state.count += 1,
        move |_| EmptyView,
    )
}


let mut app = App::new(State::default(), Size::new(200, 100), counter_view);

}
fn counter_view(state: &State) -> impl View<Rgb888, State> + use<> {
    let count = state.count;
    Button::new(
        |state: &mut State| state.count += 1,
        move |_| {
            Text::new_fmt::<48>(
                format_args!("I've been tapped {count} times!"),
                &embedded_graphics::mono_font::ascii::FONT_10X20,
            )
            .foreground_color(Rgb888::WHITE)
            .padding(Edges::All, 10)
        },
    )
}

Handling Events

The Harness trait, implemented by App, provides Harness::send(), which can be used to send events to the app. In addition to send(), a number of convenience methods are provided for constructing common events.

Here, events generated by the embedded-graphics-simulator window are mapped into Buoyant events using a MouseTracker, then sent to the app with App::send():

    loop {
        // Update the app time
        app.set_time(app_start.elapsed());

        // Process simulator events
        window
            .events()
            .filter_map(|event| {
                // The simulator won't exit if we don't handle this
                if event == embedded_graphics_simulator::SimulatorEvent::Quit {
                    exit(0);
                }
                mouse_tracker.process_event(event)
            })
            .for_each(|event| {
                app.send(event);
            });

        // rendering...
    }

Rendering

To determine whether rendering a new frame is necessary, two fields should be checked:

  • App::should_redraw(): This flag is set when the state changes or a view modifies the render tree during event handling, such as on a ScrollView drag.

  • RenderTarget::clear_animation_status(): Render tree nodes, such as animations, set a flag on the render target when they’re drawn and animation is in progress. This flag is initially set to true.

    loop {
        // event handling...

        // Only render if active animation was reported or redraw is needed
        if app.should_redraw() || target.clear_animation_status() {
            // Render animated transition between source and target trees
            app.render_animated(&mut target, &Rgb888::WHITE);

            // Send to the display
            window.update(target.display());
            // Clear for the next frame
            target.clear(Rgb888::BLACK);
        } else {
            // Optionally cap polling rate by sleeping
            std::thread::sleep(Duration::from_millis(5));
        }
    }

Mutating State Outside the View

Generally, state changed as a direct result of user input (e.g. button taps) will cause the view to be rebuilt automatically. To access the state outside the view, you can use App::state_mut() which returns a (wrapped) mutable reference to the app state. Mutably dereferencing this object will automatically trigger a view rebuild with the updated state. This is a handy convenience to prevent missing an update, but be aware of the potential to accidentally trigger rebuilds when no state actually changed.

#![allow(unused)]
fn main() {
extern crate buoyant;
extern crate embedded_graphics;
use buoyant::app::App;
use buoyant::view::prelude::*;
use buoyant::primitives::Size;
use embedded_graphics::pixelcolor::Rgb888;

#[derive(Clone, Default)]
struct State { count: i32 }

fn counter_view(state: &State) -> impl View<Rgb888, State> + use<> {
    Button::new(
        |state: &mut State| state.count += 1,
        move |_| EmptyView,
    )
}

let mut app = App::new(State::default(), Size::new(200, 100), counter_view);

// The state can be read and a mutable reference obtained
// without triggering a rebuild:

let count = app.state().count;
let mut s = app.state_mut();

// Mutably dereferencing triggers a view rebuild:

s.count = 42;
}

Render trees are rebuilt lazily upon calling App::render_animated() or App::finalize_view() if the state may have changed.

App::force_rebuild() can be used to force the view to rebuild if the state has changed without Buoyant’s knowledge, like through interior mutability.

Complete Example

extern crate buoyant;
extern crate embedded_graphics;
extern crate embedded_graphics_simulator;
use std::process::exit;
use std::time::{Duration, Instant};

use buoyant::{
    app::{App, Harness},
    event::simulator::MouseTracker,
    render_target::{EmbeddedGraphicsRenderTarget, RenderTarget as _},
    view::prelude::*,
};
use embedded_graphics::{pixelcolor::Rgb888, prelude::*};
use embedded_graphics_simulator::{OutputSettings, SimulatorDisplay, Window};

#[derive(Clone, Default)]
struct State {
    count: i32,
}

fn main() {
    let size = Size::new(200, 100);
    let mut display: SimulatorDisplay<Rgb888> = SimulatorDisplay::new(size.into());
    let mut target = EmbeddedGraphicsRenderTarget::new(&mut display);
    let mut window = Window::new("Example", &OutputSettings::default());
    // Send at least one update to the window so it doesn't panic when fetching events
    window.update(target.display());

    let app_start = Instant::now();
    // This derives higher-level mouse events from the raw simulator events
    let mut mouse_tracker = MouseTracker::new();

    // Create app with view lifecycle management
    let mut app = App::new(State::default(), size.into(), counter_view);

    // Main event loop
    loop {
        // Update the app time
        app.set_time(app_start.elapsed());

        // Process simulator events
        window
            .events()
            .filter_map(|event| {
                // The simulator won't exit if we don't handle this
                if event == embedded_graphics_simulator::SimulatorEvent::Quit {
                    exit(0);
                }
                mouse_tracker.process_event(event)
            })
            .for_each(|event| {
                app.send(event);
            });

        // Only render if active animation was reported or redraw is needed
        if app.should_redraw() || target.clear_animation_status() {
            // Render animated transition between source and target trees
            app.render_animated(&mut target, &Rgb888::WHITE);

            // Send to the display
            window.update(target.display());
            // Clear for the next frame
            target.clear(Rgb888::BLACK);
        } else {
            // Optionally cap polling rate by sleeping
            std::thread::sleep(Duration::from_millis(5));
        }
    }
}

fn counter_view(state: &State) -> impl View<Rgb888, State> + use<> {
    let count = state.count;
    Button::new(
        |state: &mut State| state.count += 1,
        move |_| {
            Text::new_fmt::<48>(
                format_args!("I've been tapped {count} times!"),
                &embedded_graphics::mono_font::ascii::FONT_10X20,
            )
            .foreground_color(Rgb888::WHITE)
            .padding(Edges::All, 10)
        },
    )
}