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                if msg.trigger_start() {
109                    if let Ok(index) = msg.sender.key().parse::<usize>() {
110                        let _ = context.state.write(TabsState {
111                            active_index: index,
112                        });
113                    }
114                }
115            }
116        }
117    })
118}
119
120#[pre_hooks(use_nav_container_active, use_nav_item, use_nav_tabs_box)]
121pub fn nav_tabs_box(mut context: WidgetContext) -> WidgetNode {
122    let WidgetContext {
123        id,
124        key,
125        props,
126        state,
127        listed_slots,
128        ..
129    } = context;
130
131    let main_props = props.read_cloned_or_default::<TabsBoxProps>();
132    let props = props.clone().with(main_props.to_main_props());
133    let tabs_props = Props::new(main_props.to_tabs_props()).with(FlexBoxItemLayout {
134        basis: main_props.tabs_basis,
135        grow: 0.0,
136        shrink: 0.0,
137        ..Default::default()
138    });
139    let TabsState { active_index } = state.read_cloned_or_default();
140    let switch_props = SwitchBoxProps {
141        active_index: if active_index < listed_slots.len() {
142            Some(active_index)
143        } else {
144            None
145        },
146        clipping: main_props.contents_clipping,
147        ..Default::default()
148    };
149    let mut tabs = Vec::<WidgetNode>::with_capacity(listed_slots.len());
150    let mut contents = Vec::with_capacity(listed_slots.len());
151
152    for (index, item) in listed_slots.into_iter().enumerate() {
153        let [mut tab, content] = item.unpack_tuple();
154        tab.remap_props(|props| {
155            props.with(TabPlateProps {
156                active: active_index == index,
157                index,
158            })
159        });
160        let props = Props::new(NavItemActive).with(ButtonNotifyProps(id.to_owned().into()));
161        tabs.push(
162            make_widget!(button)
163                .key(index)
164                .merge_props(props)
165                .named_slot("content", tab)
166                .into(),
167        );
168        contents.push(content);
169    }
170
171    make_widget!(flex_box)
172        .key(key)
173        .merge_props(props)
174        .listed_slot(
175            make_widget!(flex_box)
176                .key("tabs")
177                .merge_props(tabs_props)
178                .listed_slots(tabs),
179        )
180        .listed_slot(
181            make_widget!(switch_box)
182                .key("contents")
183                .with_props(switch_props)
184                .listed_slots(contents),
185        )
186        .into()
187}