Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Redesign template macro #14

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,4 @@ Arbitrary data can be passed into the macro using normal rust blocks.
The macro returns portable `Template` values, which can be spliced into other templates using `@{ ... }`.

Not only is the macro declarative and composable, it also supports basic incrementalization (doing partial updates to the ecs rather than rebuilding from scratch).
Building the same macro multiple times with `commands.build(template)` does only the work necessary to bring the ecs into alignment with the template.
Building the same macro multiple times only does the work necessary to bring the `entity` into alignment with the template.
171 changes: 171 additions & 0 deletions examples/scene_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
use i_cant_believe_its_not_bsn::*;

use bevy::color::palettes::*;
use bevy::prelude::*;

fn main() {
App::new()
.add_plugins(DefaultPlugins)
.add_plugins(scene_tree_plugin)
.run();
}

/// Plugin for the editor scene tree pane.
fn scene_tree_plugin(app: &mut App) {
app.add_systems(Startup, mock_setup_pane);
app.add_systems(Update, build_scene_tree);
}

const DEFAULT_BACKGROUND: Srgba = tailwind::NEUTRAL_600;
const SELECTED_BACKGROUND: Srgba = tailwind::NEUTRAL_700;
const HOVERED_BACKGROUND: Srgba = tailwind::NEUTRAL_500;

// Mock an editor pane.
fn mock_setup_pane(mut commands: Commands) {
commands.insert_resource(Selection { entity: None });

commands.spawn((Name::new("Camera"), Camera2d, Hover { count: 0 }));
commands.spawn((Name::new("Example Entity"), Hover { count: 0 }));

commands.spawn_empty().build(template! {
// A full-screen node which centers it's concent
NthTensor marked this conversation as resolved.
Show resolved Hide resolved
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
} => [
// The actual window we will use as the root of the scene tree
(
SceneTreeRoot,
Node {
flex_direction: FlexDirection::Column,
column_gap: Val::Px(2.0),
padding: UiRect::all(Val::Px(8.0)),
..default()
},
BackgroundColor(DEFAULT_BACKGROUND.into()),
);
];
});

// Add observers to drive input interaction.
commands.add_observer(select_row);
commands.add_observer(hover_over_row);
commands.add_observer(hover_leave_row);
}

// Build the scene tree row template.
fn build_scene_tree(
mut commands: Commands,
scene_tree: Single<Entity, With<SceneTreeRoot>>,
query: Query<(Entity, &Name, Option<&Hover>)>,
selection: Res<Selection>,
) {
let template: Template = query
.iter()
.map(|(entity, name, hover)| scene_tree_row(entity, name, hover, &selection))
.flatten()
.collect();

commands.entity(*scene_tree).build_children(template);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notice how this is building a template on a child of another template. Now that receipts are stored on-entity, you can apply any template to any entity and it will just work (remove any components/children included in the previous template but not the new one).

}

// Generate scene tree template.
fn scene_tree_row(
target_entity: Entity,
target_name: &str,
hover: Option<&Hover>,
selection: &Selection,
) -> Template {
let is_hovered = hover.map(|h| h.count).unwrap_or(0) > 0;
let is_selected = selection.entity == Some(target_entity);

template! {
{target_name}: (
SceneTreeRow(target_entity),
Node {
padding: UiRect::all(Val::Px(4.0)),
align_items: AlignItems::Center,
..default()
},
BorderRadius::all(Val::Px(4.0)),
BackgroundColor(
if is_selected { SELECTED_BACKGROUND.into() } else if is_hovered { HOVERED_BACKGROUND.into() } else { Color::NONE }
)
) => [
(Text::new(target_name), TextFont { font_size: 11.0, ..default() }, PickingBehavior::IGNORE);
];
}
}

/// Root UI node of the scene tree.
#[derive(Component)]
struct SceneTreeRoot;

/// A scene tree row UI node.
///
/// Contains the row's corresponding scene entity.
#[derive(Component)]
struct SceneTreeRow(Entity);

/// The selected entity
#[derive(Resource)]
struct Selection {
entity: Option<Entity>,
}

// Mark selection of scene tree rows.
fn select_row(
trigger: Trigger<Pointer<Up>>,
rows: Query<&SceneTreeRow>,
mut selection: ResMut<Selection>,
) {
if let Ok(SceneTreeRow(target_entity)) = rows.get(trigger.target) {
selection.entity = Some(*target_entity);
}
}

/// The selected entity
#[derive(Component)]
struct Hover {
count: usize,
}

// Notice how the template pattern encourages you to seperate state and
// presentation. Components used for presentation (those generated by
// `Template`) are overwriten every frame, so we have to store the state else
NthTensor marked this conversation as resolved.
Show resolved Hide resolved
// where in the ecs. Template state does not have to be derived, but it's often
// easier if it is.
//
// In this case, we store 'hover' information not on the rows themselves but on
// the entities they represent. This would let us centralize hover logic if we
// had other ways to hover entiies than the list, for example if they had
NthTensor marked this conversation as resolved.
Show resolved Hide resolved
// meshes. But I havn't bothered to implement that.

// Track hover
fn hover_over_row(
trigger: Trigger<Pointer<Over>>,
rows: Query<&SceneTreeRow>,
mut hovers: Query<&mut Hover>,
) {
if let Ok(SceneTreeRow(target_entity)) = rows.get(trigger.target) {
if let Ok(mut hover) = hovers.get_mut(*target_entity) {
hover.count += 1;
}
}
}

// Track hover
fn hover_leave_row(
trigger: Trigger<Pointer<Out>>,
rows: Query<&SceneTreeRow>,
mut hovers: Query<&mut Hover>,
) {
if let Ok(SceneTreeRow(target_entity)) = rows.get(trigger.target) {
if let Ok(mut hover) = hovers.get_mut(*target_entity) {
hover.count -= 1;
}
}
}
37 changes: 19 additions & 18 deletions examples/super-sheep-counter-2000.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,12 @@ fn sheep_plugin(app: &mut App) {

fn setup(mut commands: Commands) {
commands.spawn(Camera2d);
commands.spawn(UiRoot);
}

#[derive(Component)]
struct UiRoot;

#[derive(Component)]
struct Sheep;

Expand All @@ -34,35 +38,32 @@ enum Button {
}

// A query that pulls data from the ecs and then updates it using a template.
fn sheep_system(mut commands: Commands, sheep: Query<&Sheep>) {
fn sheep_system(mut commands: Commands, sheep: Query<&Sheep>, root: Single<Entity, With<UiRoot>>) {
let num_sheep = sheep.iter().len();

let template = template!(
{
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(5.0),
right: Val::Px(5.0),
..default()
}
} [
let template = template! {
Node {
position_type: PositionType::Absolute,
bottom: Val::Px(5.0),
right: Val::Px(5.0),
..default()
} => [
@{ counter(num_sheep, "sheep", Button::Increment, Button::Decrement) };
];
);
};

commands.build(template);
commands.entity(*root).build(template);
}

// A function that returns an ecs template.
fn counter<T: Component>(num: usize, name: &str, inc: T, dec: T) -> Template {
template! {
{ Text::new("You have ") }
[
{ TextSpan::new(format!("{num}")) };
{ TextSpan::new(format!(" {name}!")) };
Text::new("You have ") => [
TextSpan::new(format!("{num}"));
TextSpan::new(format!(" {name}!"));
];
{( Button, Text::new("Increase"), TextColor(css::GREEN.into()), inc, visible_if(num < 100) )};
{( Button, Text::new("Decrease"), TextColor(css::RED.into()), dec, visible_if(num > 0) )};
( Button, Text::new("Increase"), TextColor(css::GREEN.into()), inc, visible_if(num < 100) );
( Button, Text::new("Decrease"), TextColor(css::RED.into()), dec, visible_if(num > 0) );
}
}

Expand Down
Loading