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

Add support for retaining multiple unrelated scenes on the same entity #184

Open
wants to merge 2 commits into
base: main
Choose a base branch
from

Conversation

villor
Copy link
Contributor

@villor villor commented Feb 16, 2025

A small follow-up on #181

  • Added EntityWorldMut::retain_scene_with::<T> and EntityWorldMut::retain_child_scenes_with::<T>. T is used as a marker to store multiple receipts. This allows retention of different unrelated scenes on an entity.

Why?

While playing around with widget patterns one idea that popped was to use an immutable component as "props" for a widget/schematic, with hooks to handle the instance lifecycle. Scene retention is a pretty nice way to get functionality similar to Components in libraries like React and Vue.

(⚠️ incomplete pattern below, will keep exploring it)

Let's say we want to crate an EditorButton widget with some "props" and a ButtonState. We'll create an immutable component with an insert hook to drive the effects that should run to update/render it:

// This is the driving component for our widget, with its fields being props.
// This is the interface a consumer of this widget would use in a template.
#[derive(Component, Reflect, Default, Clone)]
#[component(immutable, on_insert = editor_button_insert)]
#[require(ButtonState)]
pub struct EditorButton {
    pub disabled: bool,
    pub variant: ButtonVariant,
    pub label: String,
}

// The internal state for our widget
#[derive(Component, Default)]
struct ButtonState {
    hovered: bool,
    pressed: bool,
}

// Insert hook, calling render and retaining the scene.
fn editor_button_insert(mut world: DeferredWorld, context: HookContext) {
    let button = world.get::<EditorButton>(context.entity).unwrap();
    let state = world.get::<ButtonState>(context.entity).unwrap();

    let scene = editor_button_render(context.entity, button, state);

    world
        .commands()
        .entity(context.entity)
        // Note the passed in generic argument. This is what allows us to use `EditorButton` in any retained scene without causing conflicts.
        .retain_scene_with::<EditorButton>(scene);
}

// Render function, looks pretty similar to a functional component in web libraries.
fn editor_button_render(id: Entity, props: &EditorButton, state: &ButtonState) -> impl Scene {
    let (text_color, bg_color) = match props.variant {
        ButtonVariant::Primary => match state.hovered {
            false => (BLUE_50, BLUE_500),
            true => (BLUE_500, BLUE_300),
        },
        // ...
    };

    let label = if state.pressed {
        format!("{} (pressed)", props.label)
    } else {
        props.label.clone()
    };

    bsn! {
        (
            Node {
                flex_direction: FlexDirection::Row,
            },
            Button,
            BackgroundColor(bg_color),
        ) [
            (
                Text(label),
                TextColor(text_color)
            ),

            // A very verbose way to handle hover state... needs better pattern/abstraction
            On(move |_: Trigger<Pointer<Over>>, mut states: Query<&mut ButtonState>, mut commands: Commands| {
                // Update state
                states.get_mut(id).unwrap().hovered = true;

                // Re-insert to trigger effect
                commands.entity(id).queue(|mut entity: EntityWorldMut| {
                    let b = entity.take::<EditorButton>().unwrap();
                    entity.insert(b);
                });
            }),

            // Out
            // ...

            // Pressed
            // ...

            // Released
            // ...
        ]
    }
}

This widget can then be used in any template like so:

bsn! {
    Node {
        position_type: PositionType::Absolute,
        // ...
    } [
        EditorButton {
            label: "Click me!"
        } [
            On(|_: Trigger<Pointer<Click>>| {
                info!("Clicked!");
            })
        ],
    ]
};

The nice thing about this pattern is that it is decoupled from the reactivity solution it uses. It could use retain_scene, or it could just spawn a scene and use systems/observers to update it, or it could use a future true reactive solution. But it doesn't matter to the consumer!

@alice-i-cecile alice-i-cecile added the C-Feature Make something new possible label Feb 17, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-Feature Make something new possible
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants