Skip to main content

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