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 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}