fyrox_ui/
searchbar.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//! Search bar widget is a text box with a "clear text" button. It is used as an input field for search functionality.
22//! Keep in mind, that it does **not** provide any built-in searching functionality by itself! See [`SearchBar`] docs
23//! for more info and usage examples.
24
25#![warn(missing_docs)]
26
27use crate::style::resource::StyleResourceExt;
28use crate::style::Style;
29use crate::widget::WidgetMessage;
30use crate::{
31    border::BorderBuilder,
32    brush::Brush,
33    button::{ButtonBuilder, ButtonMessage},
34    core::{
35        algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
36        uuid_provider, variable::InheritableVariable, visitor::prelude::*,
37    },
38    decorator::DecoratorBuilder,
39    define_constructor, define_widget_deref,
40    grid::{Column, GridBuilder, Row},
41    message::{MessageDirection, UiMessage},
42    text::TextMessage,
43    text_box::{TextBoxBuilder, TextCommitMode},
44    utils::make_cross_primitive,
45    vector_image::{Primitive, VectorImageBuilder},
46    widget::{Widget, WidgetBuilder},
47    BuildContext, Control, HorizontalAlignment, Thickness, UiNode, UserInterface,
48    VerticalAlignment,
49};
50
51use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
52use std::ops::{Deref, DerefMut};
53
54/// A set of messages that can be used to get the state of a search bar.
55#[derive(Debug, Clone, PartialEq)]
56pub enum SearchBarMessage {
57    /// Emitted when a user types something in the search bar.
58    Text(String),
59}
60
61impl SearchBarMessage {
62    define_constructor!(
63        /// Creates [`SearchBarMessage::Text`] message.
64        SearchBarMessage:Text => fn text(String), layout: false
65    );
66}
67
68/// Search bar widget is a text box with a "clear text" button. It is used as an input field for search functionality.
69/// Keep in mind, that it does **not** provide any built-in searching functionality by itself, you need to implement
70/// it manually. This widget provides a "standard" looking search bar with very little functionality.
71///
72/// ## Examples
73///
74/// ```rust
75/// # use fyrox_ui::{
76/// #     core::pool::Handle,
77/// #     message::UiMessage,
78/// #     searchbar::{SearchBarBuilder, SearchBarMessage},
79/// #     widget::WidgetBuilder,
80/// #     BuildContext, UiNode,
81/// # };
82/// #
83/// fn create_search_bar(ctx: &mut BuildContext) -> Handle<UiNode> {
84///     SearchBarBuilder::new(WidgetBuilder::new()).build(ctx)
85/// }
86///
87/// // Somewhere in a UI message loop:
88/// fn handle_ui_message(my_search_bar: Handle<UiNode>, message: &UiMessage) {
89///     // Catch the moment when the search text has changed and do the actual searching.
90///     if let Some(SearchBarMessage::Text(search_text)) = message.data() {
91///         if message.destination() == my_search_bar {
92///             let items = ["foo", "bar", "baz"];
93///
94///             println!(
95///                 "{} found at {:?} position",
96///                 search_text,
97///                 items.iter().position(|i| *i == search_text)
98///             );
99///         }
100///     }
101/// }
102/// ```
103#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
104#[reflect(derived_type = "UiNode")]
105pub struct SearchBar {
106    /// Base widget of the search bar.
107    pub widget: Widget,
108    /// A handle of a text box widget used for text input.
109    pub text_box: InheritableVariable<Handle<UiNode>>,
110    /// A handle of a button, that is used to clear the text.
111    pub clear: InheritableVariable<Handle<UiNode>>,
112}
113
114impl ConstructorProvider<UiNode, UserInterface> for SearchBar {
115    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
116        GraphNodeConstructor::new::<Self>()
117            .with_variant("Search Bar", |ui| {
118                SearchBarBuilder::new(WidgetBuilder::new().with_name("Search Bar"))
119                    .build(&mut ui.build_ctx())
120                    .into()
121            })
122            .with_group("Input")
123    }
124}
125
126define_widget_deref!(SearchBar);
127
128uuid_provider!(SearchBar = "23db1179-0e07-493d-98fd-2b3c0c795215");
129
130impl Control for SearchBar {
131    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
132        self.widget.handle_routed_message(ui, message);
133
134        if message.destination() == self.handle && message.direction() == MessageDirection::ToWidget
135        {
136            if let Some(SearchBarMessage::Text(text)) = message.data() {
137                ui.send_message(TextMessage::text(
138                    *self.text_box,
139                    MessageDirection::ToWidget,
140                    text.clone(),
141                ));
142            } else if let Some(WidgetMessage::Focus) = message.data() {
143                ui.send_message(WidgetMessage::focus(
144                    *self.text_box,
145                    MessageDirection::ToWidget,
146                ));
147            }
148        }
149
150        if message.destination() == *self.clear {
151            if let Some(ButtonMessage::Click) = message.data() {
152                ui.send_message(SearchBarMessage::text(
153                    self.handle,
154                    MessageDirection::ToWidget,
155                    String::new(),
156                ));
157            }
158        }
159
160        if message.destination() == *self.text_box
161            && message.direction() == MessageDirection::FromWidget
162        {
163            if let Some(TextMessage::Text(text)) = message.data() {
164                ui.send_message(SearchBarMessage::text(
165                    self.handle,
166                    MessageDirection::FromWidget,
167                    text.clone(),
168                ));
169            }
170        }
171    }
172}
173
174/// Search bar builder creates [`SearchBar`] widget instances and adds them to the user interface.
175pub struct SearchBarBuilder {
176    widget_builder: WidgetBuilder,
177}
178
179impl SearchBarBuilder {
180    /// Creates a new builder instance.
181    pub fn new(widget_builder: WidgetBuilder) -> Self {
182        Self { widget_builder }
183    }
184
185    /// Finishes search bar building and adds the new instance to the user interface.
186    pub fn build(mut self, ctx: &mut BuildContext) -> Handle<UiNode> {
187        // Focusing the search bar itself is useless, so we're taking the tab index from the inner
188        // widget builder and transfer it to the inner text box.
189        let tab_index = self.widget_builder.tab_index.take();
190
191        let text_box;
192        let clear;
193        let content = BorderBuilder::new(
194            WidgetBuilder::new()
195                .with_foreground(ctx.style.property(Style::BRUSH_LIGHT))
196                .with_background(ctx.style.property(Style::BRUSH_DARKER))
197                .with_child(
198                    GridBuilder::new(
199                        WidgetBuilder::new()
200                            .with_child(
201                                VectorImageBuilder::new(
202                                    WidgetBuilder::new()
203                                        .with_clip_to_bounds(false)
204                                        .with_width(12.0)
205                                        .with_height(12.0)
206                                        .with_vertical_alignment(VerticalAlignment::Center)
207                                        .with_foreground(ctx.style.property(Style::BRUSH_BRIGHT))
208                                        .with_margin(Thickness {
209                                            left: 4.0,
210                                            top: 2.0,
211                                            right: 0.0,
212                                            bottom: 0.0,
213                                        }),
214                                )
215                                .with_primitives(vec![
216                                    Primitive::WireCircle {
217                                        center: Vector2::new(4.0, 4.0),
218                                        radius: 4.0,
219                                        thickness: 1.5,
220                                        segments: 16,
221                                    },
222                                    Primitive::Line {
223                                        begin: Vector2::new(6.0, 6.0),
224                                        end: Vector2::new(11.0, 11.0),
225                                        thickness: 1.5,
226                                    },
227                                ])
228                                .build(ctx),
229                            )
230                            .with_child({
231                                text_box = TextBoxBuilder::new(
232                                    WidgetBuilder::new()
233                                        .with_tab_index(tab_index)
234                                        .on_column(1)
235                                        .with_margin(Thickness::uniform(1.0)),
236                                )
237                                .with_text_commit_mode(TextCommitMode::Immediate)
238                                .with_vertical_text_alignment(VerticalAlignment::Center)
239                                .build(ctx);
240                                text_box
241                            })
242                            .with_child({
243                                clear = ButtonBuilder::new(
244                                    WidgetBuilder::new()
245                                        .with_width(18.0)
246                                        .with_height(18.0)
247                                        .on_column(2),
248                                )
249                                .with_back(
250                                    DecoratorBuilder::new(
251                                        BorderBuilder::new(WidgetBuilder::new())
252                                            .with_pad_by_corner_radius(false)
253                                            .with_corner_radius(4.0f32.into()),
254                                    )
255                                    .with_normal_brush(Brush::Solid(Color::TRANSPARENT).into())
256                                    .build(ctx),
257                                )
258                                .with_content(
259                                    VectorImageBuilder::new(
260                                        WidgetBuilder::new()
261                                            .with_horizontal_alignment(HorizontalAlignment::Center)
262                                            .with_vertical_alignment(VerticalAlignment::Center)
263                                            .with_height(8.0)
264                                            .with_width(8.0)
265                                            .with_foreground(
266                                                ctx.style.property(Style::BRUSH_BRIGHTEST),
267                                            ),
268                                    )
269                                    .with_primitives(make_cross_primitive(8.0, 2.0))
270                                    .build(ctx),
271                                )
272                                .build(ctx);
273                                clear
274                            }),
275                    )
276                    .add_row(Row::stretch())
277                    .add_column(Column::auto())
278                    .add_column(Column::stretch())
279                    .add_column(Column::auto())
280                    .build(ctx),
281                ),
282        )
283        .with_corner_radius(4.0f32.into())
284        .with_pad_by_corner_radius(false)
285        .with_stroke_thickness(Thickness::uniform(1.0).into())
286        .build(ctx);
287
288        let search_bar = SearchBar {
289            widget: self.widget_builder.with_child(content).build(ctx),
290            text_box: text_box.into(),
291            clear: clear.into(),
292        };
293
294        ctx.add_node(UiNode::new(search_bar))
295    }
296}
297
298#[cfg(test)]
299mod test {
300    use crate::selector::SelectorBuilder;
301    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
302
303    #[test]
304    fn test_deletion() {
305        test_widget_deletion(|ctx| SelectorBuilder::new(WidgetBuilder::new()).build(ctx));
306    }
307}