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 aScrollViewdrag. -
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 totrue.
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)
},
)
}