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