raui-core 0.37.1

Renderer Agnostic User Interface
Documentation
use crate::{
    pre_hooks,
    props::Props,
    unpack_named_slots, widget,
    widget::{
        component::{
            containers::{
                content_box::{content_box, ContentBoxProps},
                size_box::{size_box, SizeBoxProps},
            },
            image_box::{image_box, ImageBoxProps},
            interactive::{
                button::{button, ButtonNotifyMessage, ButtonNotifyProps},
                navigation::{
                    use_nav_container_active, use_nav_item, use_nav_item_active,
                    use_nav_scroll_view_content, NavButtonTrackingActive, NavItemActive, NavJump,
                    NavScroll, NavSignal,
                },
                scroll_view::{use_scroll_view, ScrollViewState},
            },
            use_resize_listener, ResizeListenerSignal,
        },
        context::WidgetContext,
        node::WidgetNode,
        unit::{
            area::AreaBoxNode, content::ContentBoxItemLayout, image::ImageBoxMaterial,
            size::SizeBoxSizeValue,
        },
        utils::{lerp, Rect, Vec2},
        WidgetId,
    },
    PropsData, Scalar,
};
use serde::{Deserialize, Serialize};

#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
#[props_data(crate::props::PropsData)]
#[prefab(crate::Prefab)]
pub struct ScrollBoxOwner(
    #[serde(default)]
    #[serde(skip_serializing_if = "WidgetId::is_none")]
    pub WidgetId,
);

#[derive(PropsData, Debug, Clone, Serialize, Deserialize)]
#[props_data(crate::props::PropsData)]
#[prefab(crate::Prefab)]
pub struct SideScrollbarsProps {
    #[serde(default)]
    pub size: Scalar,
    #[serde(default)]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub back_material: Option<ImageBoxMaterial>,
    #[serde(default)]
    pub front_material: ImageBoxMaterial,
}

impl Default for SideScrollbarsProps {
    fn default() -> Self {
        Self {
            size: 10.0,
            back_material: None,
            front_material: Default::default(),
        }
    }
}

#[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
#[props_data(crate::props::PropsData)]
#[prefab(crate::Prefab)]
pub struct SideScrollbarsState {
    pub horizontal: Scalar,
    pub vertical: Scalar,
}

pub fn use_nav_scroll_box_content(context: &mut WidgetContext) {
    context.life_cycle.change(|context| {
        for msg in context.messenger.messages {
            if let Some(ResizeListenerSignal::Change(size)) = msg.as_any().downcast_ref() {
                if let Ok(data) = context.props.read::<ScrollBoxOwner>() {
                    context
                        .messenger
                        .write(data.0.to_owned(), ResizeListenerSignal::Change(*size));
                }
            }
        }
    });
}

#[pre_hooks(
    use_resize_listener,
    use_nav_item_active,
    use_nav_container_active,
    use_nav_scroll_view_content,
    use_nav_scroll_box_content
)]
pub fn nav_scroll_box_content(mut context: WidgetContext) -> WidgetNode {
    let WidgetContext {
        id, named_slots, ..
    } = context;
    unpack_named_slots!(named_slots => content);

    widget! {{{
        AreaBoxNode {
            id: id.to_owned(),
            slot: Box::new(content),
            renderer_effect: None,
        }
    }}}
}

pub fn use_nav_scroll_box(context: &mut WidgetContext) {
    context.life_cycle.change(|context| {
        for msg in context.messenger.messages {
            if let Some(ResizeListenerSignal::Change(_)) = msg.as_any().downcast_ref() {
                if let Ok(data) = context.state.read::<ScrollViewState>() {
                    context
                        .signals
                        .write(NavSignal::Jump(NavJump::Scroll(NavScroll::Factor(
                            data.value, false,
                        ))));
                }
            }
        }
    });
}

#[pre_hooks(
    use_resize_listener,
    use_nav_item,
    use_nav_container_active,
    use_scroll_view,
    use_nav_scroll_box
)]
pub fn nav_scroll_box(mut context: WidgetContext) -> WidgetNode {
    let WidgetContext {
        id,
        key,
        props,
        state,
        named_slots,
        ..
    } = context;
    unpack_named_slots!(named_slots => {content, scrollbars});

    let scroll_props = state.read_cloned_or_default::<ScrollViewState>();

    let content_props = Props::new(ContentBoxItemLayout {
        align: scroll_props.value,
        ..Default::default()
    })
    .with(ScrollBoxOwner(id.to_owned()));

    if let Some(props) = scrollbars.props_mut() {
        props.write(ScrollBoxOwner(id.to_owned()));
        props.write(scroll_props);
    }

    if !props.has::<ContentBoxProps>() {
        props.write(ContentBoxProps {
            clipping: true,
            ..Default::default()
        });
    }

    let size_props = SizeBoxProps {
        width: SizeBoxSizeValue::Fill,
        height: SizeBoxSizeValue::Fill,
        ..Default::default()
    };

    let content = widget! {
        (#{key} content_box: {props.clone()} [
            (#{"input-consumer"} button: {NavItemActive} {
                content = (#{"size"} size_box: {size_props})
            })
            (#{"content"} nav_scroll_box_content: {content_props} {
                content = {content}
            })
            {scrollbars}
        ])
    };
    widget! {{{
        AreaBoxNode {
            id: id.to_owned(),
            slot: Box::new(content),
            renderer_effect: None,
        }
    }}}
}

pub fn use_nav_scroll_box_side_scrollbars(context: &mut WidgetContext) {
    context.life_cycle.mount(|context| {
        let _ = context.state.write_with(SideScrollbarsState::default());
    });

    context.life_cycle.change(|context| {
        for msg in context.messenger.messages {
            if let Some(msg) = msg.as_any().downcast_ref::<ButtonNotifyMessage>() {
                if msg.trigger_start() {
                    context.signals.write(NavSignal::Lock);
                }
                if msg.trigger_stop() {
                    context.signals.write(NavSignal::Unlock);
                }
                if msg.state.selected && (msg.state.trigger || msg.state.context) {
                    let mut dirty = false;
                    let mut data = context
                        .state
                        .read_cloned_or_default::<SideScrollbarsState>();
                    if msg.sender.key() == "hbar" {
                        data.horizontal = msg.state.pointer.x;
                        dirty = true;
                    } else if msg.sender.key() == "vbar" {
                        data.vertical = msg.state.pointer.y;
                        dirty = true;
                    }
                    if dirty {
                        let view = context.props.read_cloned_or_default::<ScrollBoxOwner>().0;
                        let pos = Vec2 {
                            x: data.horizontal,
                            y: data.vertical,
                        };
                        context.signals.write(NavSignal::Jump(NavJump::Scroll(
                            NavScroll::DirectFactor(view.into(), pos, false),
                        )));
                        let _ = context.state.write_with(data);
                    }
                }
            }
        }
    });
}

#[pre_hooks(
    use_nav_item_active,
    use_nav_container_active,
    use_nav_scroll_box_side_scrollbars
)]
pub fn nav_scroll_box_side_scrollbars(mut context: WidgetContext) -> WidgetNode {
    let WidgetContext { id, key, props, .. } = context;

    let view_props = props.read_cloned_or_default::<ScrollViewState>();

    let SideScrollbarsProps {
        size,
        back_material,
        front_material,
    } = props.read_cloned_or_default();

    let hbar = if view_props.size_factor.x > 1.0 {
        let length = 1.0 / view_props.size_factor.y;
        let rest = 1.0 - length;

        let button_props = Props::new(ButtonNotifyProps(id.to_owned().into()))
            .with(NavItemActive)
            .with(NavButtonTrackingActive)
            .with(ContentBoxItemLayout {
                anchors: Rect {
                    left: 0.0,
                    right: 1.0,
                    top: 1.0,
                    bottom: 1.0,
                },
                margin: Rect {
                    left: 0.0,
                    right: size,
                    top: -size,
                    bottom: 0.0,
                },
                align: Vec2 { x: 0.0, y: 1.0 },
                ..Default::default()
            });

        let front_props = Props::new(ImageBoxProps {
            material: front_material.clone(),
            ..Default::default()
        })
        .with(ContentBoxItemLayout {
            anchors: Rect {
                left: lerp(0.0, rest, view_props.value.x),
                right: lerp(length, 1.0, view_props.value.x),
                top: 0.0,
                bottom: 1.0,
            },
            ..Default::default()
        });

        let back = if let Some(material) = back_material.clone() {
            let props = ImageBoxProps {
                material,
                ..Default::default()
            };

            widget! { (#{"back"} image_box: {props}) }
        } else {
            widget! {()}
        };

        widget! {
            (#{"hbar"} button: {button_props} {
                content = (#{"container"} content_box [
                    {back}
                    (#{"front"} image_box: {front_props})
                ])
            })
        }
    } else {
        widget! {()}
    };

    let vbar = if view_props.size_factor.y > 1.0 {
        let length = 1.0 / view_props.size_factor.y;
        let rest = 1.0 - length;

        let button_props = Props::new(ButtonNotifyProps(id.to_owned().into()))
            .with(NavItemActive)
            .with(NavButtonTrackingActive)
            .with(ContentBoxItemLayout {
                anchors: Rect {
                    left: 1.0,
                    right: 1.0,
                    top: 0.0,
                    bottom: 1.0,
                },
                margin: Rect {
                    left: -size,
                    right: 0.0,
                    top: 0.0,
                    bottom: size,
                },
                align: Vec2 { x: 1.0, y: 0.0 },
                ..Default::default()
            });

        let back = if let Some(material) = back_material {
            let props = ImageBoxProps {
                material,
                ..Default::default()
            };

            widget! { (#{"back"} image_box: {props}) }
        } else {
            widget! {()}
        };

        let front_props = Props::new(ImageBoxProps {
            material: front_material,
            ..Default::default()
        })
        .with(ContentBoxItemLayout {
            anchors: Rect {
                left: 0.0,
                right: 1.0,
                top: lerp(0.0, rest, view_props.value.y),
                bottom: lerp(length, 1.0, view_props.value.y),
            },
            ..Default::default()
        });

        widget! {
            (#{"vbar"} button: {button_props} {
                content = (#{"container"} content_box [
                    {back}
                    (#{"front"} image_box: {front_props})
                ])
            })
        }
    } else {
        widget! {()}
    };

    widget! {
        (#{key} content_box [
            {hbar}
            {vbar}
        ])
    }
}