raui_core/widget/component/containers/
tabs_box.rs1use 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}