fyrox_ui/selector.rs
1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Selector is a simple container widget that allows selecting an item from a fixed set of items.
22//! See [`Selector`] docs for more info.
23
24#![warn(missing_docs)]
25
26use crate::{
27 border::BorderBuilder,
28 button::{Button, ButtonBuilder, ButtonMessage},
29 core::{
30 pool::Handle, reflect::prelude::*, type_traits::prelude::*, variable::InheritableVariable,
31 visitor::prelude::*,
32 },
33 define_widget_deref,
34 grid::{Column, GridBuilder, Row},
35 message::{MessageData, MessageDirection, UiMessage},
36 utils::{make_arrow, ArrowDirection},
37 widget::{Widget, WidgetBuilder, WidgetMessage},
38 BuildContext, Control, Thickness, UiNode, UserInterface,
39};
40use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
41
42/// A set of messages that is used by [`Selector`] widget.
43#[derive(Debug, PartialEq, Clone)]
44pub enum SelectorMessage {
45 /// Adds a new item to a selector.
46 AddItem(Handle<UiNode>),
47 /// Removes an item from a selector.
48 RemoveItem(Handle<UiNode>),
49 /// Sets a new set of items of a selector.
50 SetItems {
51 /// A new set of items.
52 items: Vec<Handle<UiNode>>,
53 /// If `true` then all the previous will be deleted before setting the new items.
54 remove_previous: bool,
55 },
56 /// Sets a new current item, or gets the changes from the widget.
57 Current(Option<usize>),
58}
59impl MessageData for SelectorMessage {}
60
61/// Selector is a simple container widget that allows selecting an item from a fixed set of items.
62/// Selector widget shows the currently selected item at the center and two buttons on the sides
63/// that allows selecting either the previous or the next item.
64///
65/// ## Example
66///
67/// The following examples creates a new selector with three items and selects the middle one as
68/// active. The items can be of any type, even mixed types are allowed.
69///
70/// ```rust
71/// # use fyrox_ui::{
72/// # core::pool::{Handle, HandlesVecExtension},
73/// # selector::{Selector, SelectorBuilder},
74/// # text::TextBuilder,
75/// # widget::WidgetBuilder,
76/// # BuildContext,
77/// # };
78/// #
79/// fn create_selector(ctx: &mut BuildContext) -> Handle<Selector> {
80/// SelectorBuilder::new(WidgetBuilder::new())
81/// .with_items(
82/// vec![
83/// TextBuilder::new(WidgetBuilder::new())
84/// .with_text("Item1")
85/// .build(ctx),
86/// TextBuilder::new(WidgetBuilder::new())
87/// .with_text("Item2")
88/// .build(ctx),
89/// TextBuilder::new(WidgetBuilder::new())
90/// .with_text("Item3")
91/// .build(ctx),
92/// ]
93/// .to_base(),
94/// )
95/// .with_current_item(1)
96/// .build(ctx)
97/// }
98/// ```
99///
100/// ## Selection
101///
102/// The newly selected item index can be received from a selector by listening to [`SelectorMessage::Current`]
103/// message. To select a new item from code, send the same message with the desired index:
104///
105/// ```rust
106/// # use fyrox_ui::{
107/// # core::pool::Handle,
108/// # message::UiMessage,
109/// # selector::{Selector, SelectorMessage},
110/// # UserInterface,
111/// # };
112/// #
113/// fn on_ui_message(selector: Handle<Selector>, message: &UiMessage, ui: &UserInterface) {
114/// if let Some(SelectorMessage::Current(Some(index))) = message.data_from(selector) {
115/// println!("The new selection is {index}!");
116///
117/// if *index != 0 {
118/// // The selection can be changed by sending the same message to the widget:
119/// ui.send(selector, SelectorMessage::Current(Some(0)));
120/// }
121/// }
122/// }
123/// ```
124#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider, TypeUuidProvider)]
125#[reflect(derived_type = "UiNode")]
126#[type_uuid(id = "25118853-5c3c-4197-9e4b-2e3b9d92f4d2")]
127pub struct Selector {
128 widget: Widget,
129 items: InheritableVariable<Vec<Handle<UiNode>>>,
130 items_panel: InheritableVariable<Handle<UiNode>>,
131 current: InheritableVariable<Option<usize>>,
132 prev: InheritableVariable<Handle<Button>>,
133 next: InheritableVariable<Handle<Button>>,
134}
135
136impl ConstructorProvider<UiNode, UserInterface> for Selector {
137 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
138 GraphNodeConstructor::new::<Self>()
139 .with_variant("Selector", |ui| {
140 SelectorBuilder::new(WidgetBuilder::new().with_name("Selector"))
141 .build(&mut ui.build_ctx())
142 .to_base()
143 .into()
144 })
145 .with_group("Input")
146 }
147}
148
149define_widget_deref!(Selector);
150
151impl Control for Selector {
152 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
153 self.widget.handle_routed_message(ui, message);
154
155 if let Some(msg) = message.data::<SelectorMessage>() {
156 match msg {
157 SelectorMessage::AddItem(item) => {
158 ui.send(*item, WidgetMessage::LinkWith(*self.items_panel));
159 self.items.push(*item);
160 }
161 SelectorMessage::RemoveItem(item) => {
162 if let Some(position) = self.items.iter().position(|i| i == item) {
163 ui.send(*item, WidgetMessage::Remove);
164 self.items.remove(position);
165 }
166 }
167 SelectorMessage::SetItems {
168 items,
169 remove_previous,
170 } => {
171 if *remove_previous {
172 for &item in &*self.items {
173 ui.send(item, WidgetMessage::Remove);
174 }
175 }
176
177 for &item in items {
178 ui.send(item, WidgetMessage::LinkWith(*self.items_panel));
179 }
180
181 self.items.set_value_and_mark_modified(items.clone());
182
183 for (i, item) in self.items.iter().enumerate() {
184 ui.send(*item, WidgetMessage::Visibility(*self.current == Some(i)));
185 }
186 }
187 SelectorMessage::Current(current) => {
188 if &*self.current != current
189 && message.direction() == MessageDirection::ToWidget
190 {
191 if let Some(current) = *self.current {
192 if let Some(current_item) = self.items.get(current) {
193 ui.send(*current_item, WidgetMessage::Visibility(false));
194 }
195 }
196
197 self.current.set_value_and_mark_modified(*current);
198
199 if let Some(new_current) = *self.current {
200 if let Some(new_current_item) = self.items.get(new_current) {
201 ui.send(*new_current_item, WidgetMessage::Visibility(true));
202 }
203 }
204
205 ui.try_send_response(message);
206 }
207 }
208 }
209 } else if let Some(ButtonMessage::Click) = message.data() {
210 if message.destination() == *self.prev {
211 if let Some(current) = *self.current {
212 let new_current = current.saturating_sub(1);
213 ui.send(self.handle, SelectorMessage::Current(Some(new_current)));
214 }
215 } else if message.destination() == *self.next {
216 if let Some(current) = *self.current {
217 let new_current = current
218 .saturating_add(1)
219 .min(self.items.len().saturating_sub(1));
220 ui.send(self.handle, SelectorMessage::Current(Some(new_current)));
221 }
222 }
223 }
224 }
225}
226
227/// Creates instances of [`Selector`] widgets.
228pub struct SelectorBuilder {
229 widget_builder: WidgetBuilder,
230 items: Vec<Handle<UiNode>>,
231 current: Option<usize>,
232}
233
234impl SelectorBuilder {
235 /// Creates a new builder instance.
236 pub fn new(widget_builder: WidgetBuilder) -> Self {
237 Self {
238 widget_builder,
239 items: Default::default(),
240 current: None,
241 }
242 }
243
244 /// Sets the desired set of items for the selector.
245 pub fn with_items(mut self, items: Vec<Handle<UiNode>>) -> Self {
246 self.items = items;
247 self
248 }
249
250 /// Sets the desired selected item.
251 pub fn with_current_item(mut self, current: usize) -> Self {
252 self.current = Some(current);
253 self
254 }
255
256 /// Builds the selector.
257 pub fn build(self, ctx: &mut BuildContext) -> Handle<Selector> {
258 for (i, item) in self.items.iter().enumerate() {
259 ctx[*item].set_visibility(self.current == Some(i));
260 }
261
262 let prev;
263 let next;
264 let items_panel;
265 let grid = GridBuilder::new(
266 WidgetBuilder::new()
267 .with_child({
268 prev = ButtonBuilder::new(WidgetBuilder::new().on_column(0))
269 .with_content(make_arrow(ctx, ArrowDirection::Left, 24.0))
270 .build(ctx);
271 prev
272 })
273 .with_child({
274 items_panel = BorderBuilder::new(
275 WidgetBuilder::new()
276 .with_children(self.items.clone())
277 .on_column(1),
278 )
279 .with_stroke_thickness(Thickness::uniform(0.0).into())
280 .build(ctx);
281 items_panel
282 })
283 .with_child({
284 next = ButtonBuilder::new(WidgetBuilder::new().on_column(2))
285 .with_content(make_arrow(ctx, ArrowDirection::Right, 24.0))
286 .build(ctx);
287 next
288 }),
289 )
290 .add_row(Row::auto())
291 .add_column(Column::auto())
292 .add_column(Column::stretch())
293 .add_column(Column::auto())
294 .build(ctx);
295
296 let selector = Selector {
297 widget: self.widget_builder.with_child(grid).build(ctx),
298 items: self.items.into(),
299 items_panel: items_panel.to_base().into(),
300 prev: prev.into(),
301 next: next.into(),
302 current: self.current.into(),
303 };
304
305 ctx.add(selector)
306 }
307}
308
309#[cfg(test)]
310mod test {
311 use crate::selector::SelectorBuilder;
312 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
313
314 #[test]
315 fn test_deletion() {
316 test_widget_deletion(|ctx| SelectorBuilder::new(WidgetBuilder::new()).build(ctx));
317 }
318}