Skip to content

Commit

Permalink
Better builder doc
Browse files Browse the repository at this point in the history
  • Loading branch information
gyscos committed Jul 31, 2024
1 parent a3e0e8b commit 6069c83
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 22 deletions.
74 changes: 63 additions & 11 deletions cursive-core/src/builder.rs
Original file line number Diff line number Diff line change
@@ -1,23 +1,69 @@
//! Build views from configuration.
//!
//! # Features
//!
//! This module is only active if the `builder` feature is enabled. Otherwise, types will still be
//! exposed, blueprints can be defined, but they will be ignored.
//!
//! # Overview
//!
//! This module lets you build a view from a json-like config object.
//!
//! For example, this yaml could be parsed and used to build a basic TextView:
//!
//! ```yaml
//! TextView:
//! content: foo
//! ```
//!
//! Or, this slightly larger example could build a LinearLayout, relying on a `left_label`
//! variable that would need to be fed:
//!
//! ```yaml
//! LinearLayout:
//! orientation: horizontal
//! children:
//! - TextView: $left_label
//! - TextView: Center
//! - Button:
//! label: Right
//! callback: $Cursive.quit
//! ```
//!
//! ## Configs
//!
//! Views are described using a `Config`, which is really just an alias for a `serde_json::Value`.
//! Note that we use the json model here, but you could parse it from JSON, yaml, or almost any
//! language supported by serde.
//!
//! ## Context
//!
//! A `Context` helps building views by providing variables that can be used by configs. They also
//! keep a list of available blueprints.
//!
//! ## Blueprints
//!
//! Blueprints define how to build a view from a json-like config object.
//! At the core of the builder system, blueprints define _how_ to build views.
//!
//! A blueprint essentially ties a name to a function `fn(Config, Context) -> Result<impl View>`.
//!
//! They are defined using macros - either manually (`manual_blueprint!`) or declaratively
//! (`#[blueprint]`). When a `Context` is created, they are automatically gathered from all
//! dependencies - so third party crates can define blueprints too.
//!
//! It should be easy for third-party view to define a blueprint.
//! ## Resolving things
//!
//! ## Builders
//! Blueprints will need to parse various types from the config to build their views - strings,
//! integers, callbacks, ...
//!
//! * Users can prepare a builder `Context` to build views, which will collect all available
//! blueprints.
//! * They can optionally store named "variables" in the context (callbacks, sizes, ...).
//! * They can then load a configuration (often a yaml file) and render the view in there.
//! To do this, they will rely on the `Resolvable` trait.
//!
//! ## Details
//! # Examples
//!
//! This crate includes:
//! - A public part, always enabled.
//! - An implementation module, conditionally compiled.
//! You can see the [`builder` example][builder.rs] and its [yaml config][config].
//!
//! [builder.rs]: https://github.com/gyscos/cursive/blob/main/cursive/examples/builder.rs
//! [config]: https://github.com/gyscos/cursive/blob/main/cursive/examples/builder.yaml
#![cfg_attr(not(feature = "builder"), allow(unused))]

mod resolvable;
Expand Down Expand Up @@ -73,8 +119,11 @@ pub type BoxedVarBuilder =
Arc<dyn Fn(&serde_json::Value, &Context) -> Result<Box<dyn Any>, Error> + Send + Sync>;

/// Everything needed to prepare a view from a config.
///
/// - Current blueprints
/// - Any stored variables/callbacks
///
/// Cheap to clone (uses `Arc` internally).
#[derive(Clone)]
pub struct Context {
// TODO: Merge variables and blueprints?
Expand Down Expand Up @@ -886,10 +935,12 @@ macro_rules! manual_blueprint {
(with $name:ident, $builder:expr) => {};
($name:ident, $builder:expr) => {};
}

#[cfg(feature = "builder")]
#[macro_export]
/// Define a blueprint to build this view from a config file.
macro_rules! manual_blueprint {
// Remember to keep the inactive version above in sync
($name:ident from $config_builder:expr) => {
$crate::submit! {
$crate::builder::Blueprint {
Expand Down Expand Up @@ -941,6 +992,7 @@ macro_rules! fn_blueprint {
#[macro_export]
/// Define a macro for a variable builder.
macro_rules! fn_blueprint {
// Remember to keep the inactive version above in sync
($name: expr, $builder:expr) => {
$crate::submit! {
$crate::builder::CallbackBlueprint {
Expand Down
2 changes: 1 addition & 1 deletion cursive-macros/src/builder/blueprint.rs
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ pub fn blueprint(attrs: TokenStream, item: TokenStream) -> TokenStream {

let ident = syn::Ident::new(&attributes.name, Span::call_site());
let result = quote! {
#root::raw_blueprint!(#ident, |config, context| {
#root::manual_blueprint!(#ident, |config, context| {
Ok({ #builder })
});
};
Expand Down
29 changes: 19 additions & 10 deletions cursive/examples/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ cursive::manual_blueprint!(VSpace from {

// We can also define blueprint that build arbitrary views.
cursive::manual_blueprint!(Titled, |config, context| {
// Manual blueprints just need to return something that implements `View`.

// Fetch a string from the config
let title: String = context.resolve(&config["title"])?;

Expand All @@ -37,7 +39,10 @@ cursive::manual_blueprint!(Titled, |config, context| {
// Or we can use a declarative blueprint definition
#[cursive::blueprint(Panel::new(child), name = "WithTitle")]
struct Blueprint {
// Some fields are used in the initialization expression above.
child: BoxedView,

// Additional fields use `set_*` setters.
title: String,
}

Expand All @@ -49,18 +54,15 @@ fn main() {

// The only thing we need to know are the variables it expects.
//
// In our case, it's a title, and an on_edit callback.
// In our case, it's a title string, and two callbacks.
context.store("title", String::from("Config-driven layout example"));

// Callbacks are tricky to store and need the exact closure type.
// Here we use a helper function, `on_edit_cb`, to wrap a closure in the proper type.
context.store("on_edit", EditView::on_edit_cb(on_edit_callback));
context.store(
"randomize",
Button::new_cb(|s| {
let cb = s
.call_on_name("edit", |e: &mut EditView| e.set_content("Not so random!"))
.unwrap();
cb(s);
}),
);

// Each callback-taking function has a matching helper.
context.store("randomize", Button::new_cb(randomize));

// Load the template - here it's a yaml file.
const CONFIG: &str = include_str!("builder.yaml");
Expand All @@ -77,6 +79,13 @@ fn main() {
siv.run();
}

fn randomize(s: &mut cursive::Cursive) {
let cb = s
.call_on_name("edit", |e: &mut EditView| e.set_content("Not so random!"))
.unwrap();
cb(s);
}

// Just a regular callback for EditView::on_edit
fn on_edit_callback(siv: &mut cursive::Cursive, text: &str, cursor: usize) {
siv.call_on_name("status", |v: &mut TextView| {
Expand Down

0 comments on commit 6069c83

Please sign in to comment.