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