raui_core/widget/component/containers/
tabs_box.rs

1use crate::{
2    PropsData, Scalar, make_widget, pre_hooks,
3    props::Props,
4    widget::{
5        component::{
6            containers::{
7                flex_box::{FlexBoxProps, flex_box},
8                switch_box::{SwitchBoxProps, switch_box},
9            },
10            interactive::{
11                button::{ButtonNotifyMessage, ButtonNotifyProps, button},
12                navigation::{NavItemActive, use_nav_container_active, use_nav_item},
13            },
14        },
15        context::WidgetContext,
16        node::WidgetNode,
17        unit::flex::{FlexBoxDirection, FlexBoxItemLayout},
18        utils::Transform,
19    },
20};
21use serde::{Deserialize, Serialize};
22
23#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
24pub enum TabsBoxTabsLocation {
25    #[default]
26    Top,
27    Bottom,
28    Left,
29    Right,
30}
31
32#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
33#[props_data(crate::props::PropsData)]
34#[prefab(crate::Prefab)]
35pub struct TabsBoxProps {
36    #[serde(default)]
37    pub tabs_location: TabsBoxTabsLocation,
38    #[serde(default)]
39    pub tabs_and_content_separation: Scalar,
40    #[serde(default)]
41    pub tabs_basis: Option<Scalar>,
42    #[serde(default)]
43    pub contents_clipping: bool,
44    #[serde(default)]
45    pub start_index: usize,
46    #[serde(default)]
47    pub transform: Transform,
48}
49
50#[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
51#[props_data(crate::props::PropsData)]
52#[prefab(crate::Prefab)]
53pub struct TabsState {
54    #[serde(default)]
55    pub active_index: usize,
56}
57
58#[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
59#[props_data(crate::props::PropsData)]
60#[prefab(crate::Prefab)]
61pub struct TabPlateProps {
62    #[serde(default)]
63    pub active: bool,
64    #[serde(default)]
65    pub index: usize,
66}
67
68impl TabsBoxProps {
69    fn to_main_props(&self) -> FlexBoxProps {
70        FlexBoxProps {
71            direction: match self.tabs_location {
72                TabsBoxTabsLocation::Top => FlexBoxDirection::VerticalTopToBottom,
73                TabsBoxTabsLocation::Bottom => FlexBoxDirection::VerticalBottomToTop,
74                TabsBoxTabsLocation::Left => FlexBoxDirection::HorizontalLeftToRight,
75                TabsBoxTabsLocation::Right => FlexBoxDirection::HorizontalRightToLeft,
76            },
77            separation: self.tabs_and_content_separation,
78            wrap: false,
79            transform: self.transform.to_owned(),
80        }
81    }
82
83    fn to_tabs_props(&self) -> FlexBoxProps {
84        FlexBoxProps {
85            direction: match self.tabs_location {
86                TabsBoxTabsLocation::Top => FlexBoxDirection::HorizontalLeftToRight,
87                TabsBoxTabsLocation::Bottom => FlexBoxDirection::HorizontalLeftToRight,
88                TabsBoxTabsLocation::Left => FlexBoxDirection::VerticalTopToBottom,
89                TabsBoxTabsLocation::Right => FlexBoxDirection::VerticalTopToBottom,
90            },
91            ..Default::default()
92        }
93    }
94}
95
96pub fn use_nav_tabs_box(context: &mut WidgetContext) {
97    context.life_cycle.mount(|context| {
98        let _ = context.state.write(TabsState {
99            active_index: context
100                .props
101                .map_or_default::<TabsBoxProps, _, _>(|p| p.start_index),
102        });
103    });
104
105    context.life_cycle.change(|context| {
106        for msg in context.messenger.messages {
107            if let Some(msg) = msg.as_any().downcast_ref::<ButtonNotifyMessage>()
108                && msg.trigger_start()
109                && let Ok(index) = msg.sender.key().parse::<usize>()
110            {
111                let _ = context.state.write(TabsState {
112                    active_index: index,
113                });
114            }
115        }
116    })
117}
118
119#[pre_hooks(use_nav_container_active, use_nav_item, use_nav_tabs_box)]
120pub fn nav_tabs_box(mut context: WidgetContext) -> WidgetNode {
121    let WidgetContext {
122        id,
123        key,
124        props,
125        state,
126        listed_slots,
127        ..
128    } = context;
129
130    let main_props = props.read_cloned_or_default::<TabsBoxProps>();
131    let props = props.clone().with(main_props.to_main_props());
132    let tabs_props = Props::new(main_props.to_tabs_props()).with(FlexBoxItemLayout {
133        basis: main_props.tabs_basis,
134        grow: 0.0,
135        shrink: 0.0,
136        ..Default::default()
137    });
138    let TabsState { active_index } = state.read_cloned_or_default();
139    let switch_props = SwitchBoxProps {
140        active_index: if active_index < listed_slots.len() {
141            Some(active_index)
142        } else {
143            None
144        },
145        clipping: main_props.contents_clipping,
146        ..Default::default()
147    };
148    let mut tabs = Vec::<WidgetNode>::with_capacity(listed_slots.len());
149    let mut contents = Vec::with_capacity(listed_slots.len());
150
151    for (index, item) in listed_slots.into_iter().enumerate() {
152        let [mut tab, content] = item.unpack_tuple();
153        tab.remap_props(|props| {
154            props.with(TabPlateProps {
155                active: active_index == index,
156                index,
157            })
158        });
159        let props = Props::new(NavItemActive).with(ButtonNotifyProps(id.to_owned().into()));
160        tabs.push(
161            make_widget!(button)
162                .key(index)
163                .merge_props(props)
164                .named_slot("content", tab)
165                .into(),
166        );
167        contents.push(content);
168    }
169
170    make_widget!(flex_box)
171        .key(key)
172        .merge_props(props)
173        .listed_slot(
174            make_widget!(flex_box)
175                .key("tabs")
176                .merge_props(tabs_props)
177                .listed_slots(tabs),
178        )
179        .listed_slot(
180            make_widget!(switch_box)
181                .key("contents")
182                .with_props(switch_props)
183                .listed_slots(contents),
184        )
185        .into()
186}