oxygengine-user-interface 0.46.1

User Interface module for Oxygengine
Documentation
use crate::{
    component::UserInterfaceView,
    resource::{input_mappings::*, ApplicationData, UserInterface},
    ui_theme_asset_protocol::UiThemeAsset,
    FeedProcessContext,
};
use core::{
    app::AppLifeCycle,
    assets::{asset::AssetId, database::AssetsDatabase},
    ecs::{AccessType, Comp, ResQuery, ResQueryItem, Universe, UnsafeRef, UnsafeScope, WorldRef},
};
use input::{
    component::InputStackInstance,
    resources::stack::{InputStack, InputStackListener},
};
use raui_core::{
    application::{Application, ProcessContext},
    interactive::default_interactions_engine::{Interaction, PointerButton},
    layout::default_layout_engine::DefaultLayoutEngine,
    widget::{
        component::interactive::navigation::{NavSignal, NavTextChange},
        setup as core_setup,
        utils::Vec2,
    },
};
use raui_material::{setup as material_setup, theme::ThemeProps};
use std::collections::HashMap;

#[derive(Default)]
pub struct UserInterfaceSystemCache {
    themes_cache: HashMap<String, ThemeProps>,
    themes_table: HashMap<AssetId, String>,
}

impl UserInterfaceSystemCache {
    pub fn theme(&self, id: &str) -> Option<&ThemeProps> {
        self.themes_cache.get(id)
    }
}

pub type UserInterfaceSystemResources<'a, Q> = (
    Q,
    WorldRef,
    &'a AppLifeCycle,
    &'a AssetsDatabase,
    &'a InputStack,
    &'a mut UserInterface,
    &'a mut UserInterfaceSystemCache,
    Comp<&'a mut UserInterfaceView>,
    Comp<&'a InputStackInstance>,
);

pub fn user_interface_system<Q>(universe: &mut Universe)
where
    Q: AccessType + ResQuery + 'static,
    ResQueryItem<Q>: FeedProcessContext,
{
    let mut cache = universe.expect_resource_mut::<UserInterfaceSystemCache>();
    {
        let assets = universe.expect_resource::<AssetsDatabase>();
        for id in assets.lately_loaded_protocol("ui-theme") {
            let id = *id;
            let asset = assets
                .asset_by_id(id)
                .expect("trying to use not loaded UI theme asset");
            let path = asset.path().to_owned();
            let asset = asset
                .get::<UiThemeAsset>()
                .expect("trying to use non UI theme asset");
            cache.themes_cache.insert(path.clone(), asset.get().props());
            cache.themes_table.insert(id, path);
        }
        for id in assets.lately_unloaded_protocol("ui-theme") {
            if let Some(path) = cache.themes_table.remove(id) {
                cache.themes_cache.remove(&path);
            }
        }
    }

    let mut ui = universe.expect_resource_mut::<UserInterface>();
    let scope = UnsafeScope;
    let meta = {
        let world = universe.world();
        let input_stack = universe.expect_resource::<InputStack>();

        ui.data.retain(|k, _| {
            world
                .query::<&UserInterfaceView>()
                .iter()
                .any(|(_, v)| k == v.app_id())
        });

        for (_, (view, input)) in world
            .query::<(&mut UserInterfaceView, Option<&InputStackInstance>)>()
            .iter()
        {
            if !ui.data.contains_key(view.app_id()) {
                let mut application = Application::new();
                application.setup(core_setup);
                application.setup(material_setup);
                if let Some(setup_application) = ui.setup_application {
                    setup_application(&mut application);
                }
                ui.data.insert(
                    view.app_id().to_owned(),
                    ApplicationData {
                        application,
                        interactions: Default::default(),
                        coords_mapping: Default::default(),
                        signals_received: Default::default(),
                    },
                );
            }

            if let Some(listener) = input
                .and_then(|input| input.as_listener())
                .and_then(|id| input_stack.listener(id))
            {
                apply_inputs(
                    ui.get_mut(view.app_id()).unwrap(),
                    listener,
                    &mut view.last_pointer_pos,
                );
            }

            if view.dirty {
                view.dirty = false;
                let app = ui.application_mut(view.app_id()).unwrap();
                let mut root = app
                    .deserialize_node(view.root().clone())
                    .expect("Could not deserialize UI node");
                if let Some(theme) = view.theme() {
                    if let Some(theme) = cache.themes_cache.get(theme) {
                        if let Some(p) = root.shared_props_mut() {
                            p.write(theme.clone());
                        }
                    }
                }
                app.apply(root);
            }
        }

        let result = world
            .query::<&UserInterfaceView>()
            .iter()
            .map(|(_, v)| unsafe { UnsafeRef::upgrade(&scope, v) })
            .collect::<Vec<_>>();
        result
    };

    let dt = universe
        .expect_resource::<AppLifeCycle>()
        .delta_time_seconds();
    let mut context = ProcessContext::new();
    let extras = universe.query_resources::<Q>();
    extras.feed_process_context(&mut context);

    for view in meta {
        if let Some(data) = ui.data.get_mut(unsafe { view.read().app_id() }) {
            data.application.animations_delta_time = dt;
            data.application.process_with_context(&mut context);
            data.application
                .layout(&data.coords_mapping, &mut DefaultLayoutEngine)
                .unwrap_or_default();
            data.interactions.deselect_when_no_button_found =
                unsafe { view.read().deselect_when_no_button_found };
            let _ = data.application.interact(&mut data.interactions);
            data.signals_received = data.application.consume_signals();
        }
    }
}

fn apply_inputs(
    data: &mut ApplicationData,
    listener: &InputStackListener,
    last_pointer_pos: &mut Vec2,
) {
    let pointer_pos = listener.axes_state_or_default::<2>(NAV_POINTER_AXES);
    let pointer_pos = Vec2 {
        x: pointer_pos[0],
        y: pointer_pos[1],
    };
    let pointer_moved = (pointer_pos.x - last_pointer_pos.x).abs() > 1.0e-6
        || (pointer_pos.y - last_pointer_pos.y).abs() > 1.0e-6;
    *last_pointer_pos = pointer_pos;
    let pointer_pos = data.coords_mapping.real_to_virtual_vec2(pointer_pos, false);
    if pointer_moved {
        data.interactions
            .interact(Interaction::PointerMove(pointer_pos));
    }

    let trigger = listener.trigger_state_or_default(NAV_POINTER_ACTION_TRIGGER);
    if trigger.is_pressed() {
        data.interactions.interact(Interaction::PointerDown(
            PointerButton::Trigger,
            pointer_pos,
        ));
    } else if trigger.is_released() {
        data.interactions
            .interact(Interaction::PointerUp(PointerButton::Trigger, pointer_pos));
    }

    let trigger = listener.trigger_state_or_default(NAV_POINTER_CONTEXT_TRIGGER);
    if trigger.is_pressed() {
        data.interactions.interact(Interaction::PointerDown(
            PointerButton::Context,
            pointer_pos,
        ));
    } else if trigger.is_released() {
        data.interactions
            .interact(Interaction::PointerUp(PointerButton::Context, pointer_pos));
    }

    for c in listener.text_state_or_default().chars() {
        if !c.is_control() {
            data.interactions
                .interact(Interaction::Navigate(NavSignal::TextChange(
                    NavTextChange::InsertCharacter(c),
                )));
        } else if c == '\n' || c == '\r' {
            data.interactions
                .interact(Interaction::Navigate(NavSignal::TextChange(
                    NavTextChange::NewLine,
                )));
        }
    }
    if listener
        .trigger_state_or_default(NAV_TEXT_MOVE_CURSOR_LEFT_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::TextChange(
                NavTextChange::MoveCursorLeft,
            )));
    }
    if listener
        .trigger_state_or_default(NAV_TEXT_MOVE_CURSOR_RIGHT_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::TextChange(
                NavTextChange::MoveCursorRight,
            )));
    }
    if listener
        .trigger_state_or_default(NAV_TEXT_MOVE_CURSOR_START_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::TextChange(
                NavTextChange::MoveCursorStart,
            )));
    }
    if listener
        .trigger_state_or_default(NAV_TEXT_MOVE_CURSOR_END_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::TextChange(
                NavTextChange::MoveCursorEnd,
            )));
    }
    if listener
        .trigger_state_or_default(NAV_TEXT_DELETE_LEFT_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::TextChange(
                NavTextChange::DeleteLeft,
            )));
    }
    if listener
        .trigger_state_or_default(NAV_TEXT_DELETE_RIGHT_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::TextChange(
                NavTextChange::DeleteRight,
            )));
    }

    let trigger = listener.trigger_state_or_default(NAV_ACCEPT_TRIGGER);
    if trigger.is_pressed() {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Accept(true)));
    } else if trigger.is_released() {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Accept(false)));
    }

    let trigger = listener.trigger_state_or_default(NAV_CANCEL_TRIGGER);
    if trigger.is_pressed() {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Cancel(true)));
    } else if trigger.is_released() {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Cancel(false)));
    }

    if listener
        .trigger_state_or_default(NAV_UP_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Up));
    }
    if listener
        .trigger_state_or_default(NAV_DOWN_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Down));
    }
    if listener
        .trigger_state_or_default(NAV_LEFT_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Left));
    }
    if listener
        .trigger_state_or_default(NAV_RIGHT_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Right));
    }
    if listener
        .trigger_state_or_default(NAV_PREV_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Prev));
    }
    if listener
        .trigger_state_or_default(NAV_NEXT_TRIGGER)
        .is_pressed()
    {
        data.interactions
            .interact(Interaction::Navigate(NavSignal::Next));
    }
}