Skip to content
Snippets Groups Projects
context.rs 14.3 KiB
Newer Older
Louis's avatar
Louis committed
//! This example demonstrates how to use the provider/consumer pattern for passing props down
//! to multiple descendants.
//!
//! The problem we'll be solving here is adding support for theming.
//!
//! One reason the provider/consumer pattern might be favored over a global state is that it allows
//! for better specificity and makes local contexts much easier to manage. In the case of theming,
//! this allows us to have multiple active themes, even if they are nested within each other!

use bevy::prelude::*;
use kayak_ui::prelude::{widgets::*, KStyle, *};

/// The color theme struct we will be using across our demo widgets
#[derive(Component, Debug, Default, Clone, PartialEq)]
struct Theme {
    name: String,
    primary: Color,
    secondary: Color,
    background: Color,
}

impl Theme {
    fn vampire() -> Self {
        Self {
            name: "Vampire".to_string(),
            primary: Color::rgba(1.0, 0.475, 0.776, 1.0),
            secondary: Color::rgba(0.641, 0.476, 0.876, 1.0),
            background: Color::rgba(0.157, 0.165, 0.212, 1.0),
        }
    }
    fn solar() -> Self {
        Self {
            name: "Solar".to_string(),
            primary: Color::rgba(0.514, 0.580, 0.588, 1.0),
            secondary: Color::rgba(0.149, 0.545, 0.824, 1.0),
            background: Color::rgba(0.026, 0.212, 0.259, 1.0),
        }
    }
    fn vector() -> Self {
        Self {
            name: "Vector".to_string(),
            primary: Color::rgba(0.533, 1.0, 0.533, 1.0),
            secondary: Color::rgba(0.098, 0.451, 0.098, 1.0),
            background: Color::rgba(0.004, 0.059, 0.004, 1.0),
        }
    }
}

#[derive(Component, Debug, Default, Clone, PartialEq)]
struct ThemeButton {
    pub theme: Theme,
}
impl Widget for ThemeButton {}

#[derive(Bundle)]
pub struct ThemeButtonBundle {
    theme_button: ThemeButton,
    styles: KStyle,
    widget_name: WidgetName,
}

impl Default for ThemeButtonBundle {
    fn default() -> Self {
        Self {
            theme_button: Default::default(),
            styles: KStyle::default(),
            widget_name: ThemeButton::default().get_name(),
        }
    }
}

fn update_theme_button(
    In(theme_button_entity): In<Entity>,
    widget_context: Res<KayakWidgetContext>,
    mut commands: Commands,
    query: Query<&ThemeButton>,
    mut context_query: Query<&mut Theme>,
) -> bool {
    if let Ok(theme_button) = query.get(theme_button_entity) {
        if let Some(theme_context_entity) =
            widget_context.get_context_entity::<Theme>(theme_button_entity)
        {
            if let Ok(theme) = context_query.get_mut(theme_context_entity) {
                let mut box_style = KStyle {
                    width: StyleProp::Value(Units::Pixels(30.0)),
                    height: StyleProp::Value(Units::Pixels(30.0)),
                    background_color: StyleProp::Value(theme_button.theme.primary),
                    ..Default::default()
                };

                if theme_button.theme.name == theme.name {
                    box_style.top = StyleProp::Value(Units::Pixels(3.0));
                    box_style.left = StyleProp::Value(Units::Pixels(3.0));
                    box_style.bottom = StyleProp::Value(Units::Pixels(3.0));
                    box_style.right = StyleProp::Value(Units::Pixels(3.0));
                    box_style.width = StyleProp::Value(Units::Pixels(24.0));
                    box_style.height = StyleProp::Value(Units::Pixels(24.0));
                }

                let parent_id = Some(theme_button_entity);
                rsx! {
                    <BackgroundBundle
                        styles={box_style}
                        on_event={OnEvent::new(
                            move |In(_entity): In<Entity>,
                            event: ResMut<KEvent>,
                            query: Query<&ThemeButton>,
                            mut context_query: Query<&mut Theme>,
                            | {
                                if let EventType::Click(..) = event.event_type {
                                    if let Ok(button) = query.get(theme_button_entity) {
                                        if let Ok(mut context_theme) = context_query.get_mut(theme_context_entity) {
                                            *context_theme = button.theme.clone();
                                        }
                                    }
                                }
                            },
                        )}
                    />
                };
            }
        }
    }

    true
}

#[derive(Component, Debug, Default, Clone, PartialEq)]
struct ThemeSelector;
impl Widget for ThemeSelector {}

#[derive(Bundle)]
pub struct ThemeSelectorBundle {
    theme_selector: ThemeSelector,
    styles: KStyle,
    widget_name: WidgetName,
}

impl Default for ThemeSelectorBundle {
    fn default() -> Self {
        Self {
            theme_selector: Default::default(),
            styles: KStyle {
                height: StyleProp::Value(Units::Auto),
                padding_bottom: Units::Pixels(40.0).into(),
                ..Default::default()
            },
            widget_name: ThemeSelector.get_name(),
        }
    }
}

fn update_theme_selector(
    In(entity): In<Entity>,
    widget_context: Res<KayakWidgetContext>,
    mut commands: Commands,
    query: Query<&ThemeSelector>,
) -> bool {
    if query.get(entity).is_ok() {
        let button_container_style = KStyle {
            layout_type: StyleProp::Value(LayoutType::Row),
            width: StyleProp::Value(Units::Stretch(1.0)),
            height: StyleProp::Value(Units::Auto),
            top: StyleProp::Value(Units::Pixels(5.0)),
            ..Default::default()
        };

        let vampire_theme = Theme::vampire();
        let solar_theme = Theme::solar();
        let vector_theme = Theme::vector();

        let parent_id = Some(entity);
        rsx! {
            <ElementBundle styles={button_container_style}>
                <ThemeButtonBundle theme_button={ThemeButton { theme: vampire_theme }} />
                <ThemeButtonBundle theme_button={ThemeButton { theme: solar_theme }} />
                <ThemeButtonBundle theme_button={ThemeButton { theme: vector_theme }} />
            </ElementBundle>
        };
    }

    true
}

#[derive(Component, Debug, Default, Clone, PartialEq, Eq)]
pub struct ThemeDemo {
    is_root: bool,
    context_entity: Option<Entity>,
}
impl Widget for ThemeDemo {}

#[derive(Bundle)]
pub struct ThemeDemoBundle {
    theme_demo: ThemeDemo,
    styles: KStyle,
    widget_name: WidgetName,
}

impl Default for ThemeDemoBundle {
    fn default() -> Self {
        Self {
            theme_demo: Default::default(),
            styles: KStyle::default(),
            widget_name: ThemeDemo::default().get_name(),
        }
    }
}

fn update_theme_demo(
    In(entity): In<Entity>,
    widget_context: Res<KayakWidgetContext>,
    mut commands: Commands,
    mut query_set: Query<&mut ThemeDemo>,
    theme_context: Query<&Theme>,
) -> bool {
    if let Ok(mut theme_demo) = query_set.get_mut(entity) {
        if let Some(theme_context_entity) = widget_context.get_context_entity::<Theme>(entity) {
            if let Ok(theme) = theme_context.get(theme_context_entity) {
                let select_lbl = if theme_demo.is_root {
                    format!("Select Theme (Current: {})", theme.name)
                } else {
                    format!("Select A Different Theme (Current: {})", theme.name)
                };

                if theme_demo.is_root && theme_demo.context_entity.is_none() {
                    let theme_entity = commands.spawn(Theme::vector()).id();
                    theme_demo.context_entity = Some(theme_entity);
                }

                let context_entity = if let Some(entity) = theme_demo.context_entity {
                    entity
                } else {
                    Entity::from_raw(1000000)
                };
                let text_styles = KStyle {
                    color: StyleProp::Value(theme.primary),
                    height: StyleProp::Value(Units::Pixels(28.0)),
                    ..Default::default()
                };
                let btn_style = KStyle {
                    background_color: StyleProp::Value(theme.secondary),
                    width: StyleProp::Value(Units::Stretch(0.75)),
                    height: StyleProp::Value(Units::Pixels(32.0)),
                    top: StyleProp::Value(Units::Pixels(5.0)),
                    left: StyleProp::Value(Units::Stretch(1.0)),
                    right: StyleProp::Value(Units::Stretch(1.0)),
                    ..Default::default()
                };

                let parent_id = Some(entity);
                rsx! {
                    <ElementBundle>
                        <TextWidgetBundle
                            styles={KStyle {
                                height: StyleProp::Value(Units::Pixels(28.0)),
                                ..Default::default()
                            }}
                            text={TextProps {
                                content: select_lbl,
                                size: 14.0,
                                line_height: Some(28.0),
                                ..Default::default()
                            }}
                        />
                        <ThemeSelectorBundle />
                        <BackgroundBundle
                            styles={KStyle {
                                background_color: StyleProp::Value(theme.background),
                                top: StyleProp::Value(Units::Pixels(15.0)),
                                width: StyleProp::Value(Units::Stretch(1.0)),
                                height: StyleProp::Value(Units::Stretch(1.0)),
                                ..Default::default()
                            }}
                        >
                            <TextWidgetBundle
                                styles={text_styles}
                                text={TextProps {
                                    content: "Lorem ipsum dolor...".into(),
                                    size: 12.0,
                                    ..Default::default()
                                }}
                            />
                            <KButtonBundle
                                button={KButton { text: "BUTTON".into() }}
                                styles={btn_style}
                            />
                            {
                                if theme_demo.is_root {
                                    widget_context.set_context_entity::<Theme>(
                                        parent_id,
                                        context_entity,
                                    );
                                    constructor! {
                                        <ElementBundle
                                            styles={KStyle {
                                                top: StyleProp::Value(Units::Pixels(10.0)),
                                                left: StyleProp::Value(Units::Pixels(10.0)),
                                                bottom: StyleProp::Value(Units::Pixels(10.0)),
                                                right: StyleProp::Value(Units::Pixels(10.0)),
                                                ..Default::default()
                                            }}
                                        >
                                            <ThemeDemoBundle />
                                        </ElementBundle>
                                    }
                                }
                            }
                        </BackgroundBundle>
                    </ElementBundle>
                };
            }
        }
    }

    true
}

fn startup(
    mut commands: Commands,
    mut font_mapping: ResMut<FontMapping>,
    asset_server: Res<AssetServer>,
) {
    let camera_entity = commands
        .spawn((Camera2dBundle::default(), CameraUIKayak))
        .id();

    font_mapping.set_default(asset_server.load("roboto.kayak_font"));

    let mut widget_context = KayakRootContext::new(camera_entity);
    widget_context.add_plugin(KayakWidgetsContextPlugin);
    widget_context.add_widget_data::<ThemeDemo, EmptyState>();
    widget_context.add_widget_data::<ThemeButton, EmptyState>();
    widget_context.add_widget_data::<ThemeSelector, EmptyState>();
    widget_context.add_widget_system(
        ThemeDemo::default().get_name(),
        widget_update_with_context::<ThemeDemo, EmptyState, Theme>,
        update_theme_demo,
    );
    widget_context.add_widget_system(
        ThemeButton::default().get_name(),
        widget_update_with_context::<ThemeButton, EmptyState, Theme>,
        update_theme_button,
    );
    widget_context.add_widget_system(
        ThemeSelector.get_name(),
        widget_update_with_context::<ThemeSelector, EmptyState, Theme>,
        update_theme_selector,
    );
    let parent_id = None;
    rsx! {
        <KayakAppBundle>
            {
                let theme_entity = commands.spawn(Theme::vampire()).id();
                widget_context.set_context_entity::<Theme>(parent_id, theme_entity);
            }
            <WindowBundle
                window={KWindow {
                    title: "Context Example".into(),
                    draggable: true,
                    initial_position: Vec2::ZERO,
                    size: Vec2::new(350.0, 400.0),
                    ..Default::default()
                }}
            >
                <ThemeDemoBundle
                    theme_demo={ThemeDemo {
                        is_root: true,
                        context_entity: None,
                    }}
                />
            </WindowBundle>
        </KayakAppBundle>
    };

    commands.spawn((widget_context, EventDispatcher::default()));
}

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_plugins((KayakContextPlugin, KayakWidgets))
        .add_systems(Startup, startup)
        .run()
}