raui_core/widget/component/containers/
tabs_box.rs

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