fyrox_ui/button.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//! Defines a clickable widget with arbitrary content. See [`Button`] dos for more info and examples.
22
23#![warn(missing_docs)]
24
25use crate::style::StyledProperty;
26use crate::{
27 border::BorderBuilder,
28 core::{
29 pool::Handle, reflect::prelude::*, type_traits::prelude::*, variable::InheritableVariable,
30 visitor::prelude::*,
31 },
32 decorator::DecoratorBuilder,
33 define_constructor,
34 font::FontResource,
35 message::{KeyCode, MessageDirection, UiMessage},
36 style::{resource::StyleResourceExt, Style},
37 text::TextBuilder,
38 widget::{Widget, WidgetBuilder, WidgetMessage},
39 BuildContext, Control, HorizontalAlignment, Thickness, UiNode, UserInterface,
40 VerticalAlignment,
41};
42use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
43use std::{
44 cell::RefCell,
45 ops::{Deref, DerefMut},
46};
47
48/// Messages that can be emitted by [`Button`] widget (or can be sent to the widget).
49#[derive(Debug, Clone, PartialEq)]
50pub enum ButtonMessage {
51 /// Emitted by the button widget when it was clicked by any mouse button. Click is a press with a following release
52 /// of a mouse button withing the button bounds. This message can be only emitted, not sent. See [`Button`] docs
53 /// for usage examples.
54 Click,
55 /// A message, that can be used to set new content of the button. See [`ButtonContent`] for usage examples.
56 Content(ButtonContent),
57 /// Click repetition interval (in seconds) of the button. The button will send [`ButtonMessage::Click`] with the
58 /// desired period.
59 RepeatInterval(f32),
60 /// A flag, that defines whether the button should repeat click message when being hold or not.
61 RepeatClicksOnHold(bool),
62}
63
64impl ButtonMessage {
65 define_constructor!(
66 /// A shortcut method to create [`ButtonMessage::Click`] message.
67 ButtonMessage:Click => fn click(), layout: false
68 );
69 define_constructor!(
70 /// A shortcut method to create [`ButtonMessage::Content`] message.
71 ButtonMessage:Content => fn content(ButtonContent), layout: false
72 );
73 define_constructor!(
74 /// A shortcut method to create [`ButtonMessage::RepeatInterval`] message.
75 ButtonMessage:RepeatInterval => fn repeat_interval(f32), layout: false
76 );
77 define_constructor!(
78 /// A shortcut method to create [`ButtonMessage::RepeatClicksOnHold`] message.
79 ButtonMessage:RepeatClicksOnHold => fn repeat_clicks_on_hold(bool), layout: false
80 );
81}
82
83/// Defines a clickable widget with arbitrary content. The content could be any kind of widget, usually it
84/// is just a text or an image.
85///
86/// ## Examples
87///
88/// To create a simple button with text you should do something like this:
89///
90/// ```rust
91/// # use fyrox_ui::{
92/// # core::pool::Handle,
93/// # button::ButtonBuilder, widget::WidgetBuilder, UiNode, UserInterface
94/// # };
95/// fn create_button(ui: &mut UserInterface) -> Handle<UiNode> {
96/// ButtonBuilder::new(WidgetBuilder::new())
97/// .with_text("Click me!")
98/// .build(&mut ui.build_ctx())
99/// }
100/// ```
101///
102/// To do something when your button was clicked you need to "listen" to user interface messages from the
103/// queue and check if there's [`ButtonMessage::Click`] message from your button:
104///
105/// ```rust
106/// # use fyrox_ui::{button::ButtonMessage, core::pool::Handle, message::UiMessage};
107/// fn on_ui_message(message: &UiMessage) {
108/// # let your_button_handle = Handle::NONE;
109/// if let Some(ButtonMessage::Click) = message.data() {
110/// if message.destination() == your_button_handle {
111/// println!("{} button was clicked!", message.destination());
112/// }
113/// }
114/// }
115/// ```
116#[derive(Default, Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)]
117#[type_uuid(id = "2abcf12b-2f19-46da-b900-ae8890f7c9c6")]
118#[reflect(derived_type = "UiNode")]
119pub struct Button {
120 /// Base widget of the button.
121 pub widget: Widget,
122 /// Current content holder of the button.
123 pub decorator: InheritableVariable<Handle<UiNode>>,
124 /// Current content of the button. It is attached to the content holder.
125 pub content: InheritableVariable<Handle<UiNode>>,
126 /// Click repetition interval (in seconds) of the button.
127 #[visit(optional)]
128 #[reflect(min_value = 0.0)]
129 pub repeat_interval: InheritableVariable<f32>,
130 /// Current clicks repetition timer.
131 #[visit(optional)]
132 #[reflect(hidden)]
133 pub repeat_timer: RefCell<Option<f32>>,
134 /// A flag, that defines whether the button should repeat click message when being
135 /// hold or not. Default is `false` (disabled).
136 #[visit(optional)]
137 pub repeat_clicks_on_hold: InheritableVariable<bool>,
138}
139
140impl Button {
141 /// A name of style property, that defines corner radius of a button.
142 pub const CORNER_RADIUS: &'static str = "Button.CornerRadius";
143 /// A name of style property, that defines border thickness of a button.
144 pub const BORDER_THICKNESS: &'static str = "Button.BorderThickness";
145
146 /// Returns a style of the widget. This style contains only widget-specific properties.
147 pub fn style() -> Style {
148 Style::default()
149 .with(Self::CORNER_RADIUS, 4.0f32)
150 .with(Self::BORDER_THICKNESS, Thickness::uniform(1.0))
151 }
152}
153
154impl ConstructorProvider<UiNode, UserInterface> for Button {
155 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
156 GraphNodeConstructor::new::<Self>()
157 .with_variant("Button", |ui| {
158 ButtonBuilder::new(
159 WidgetBuilder::new()
160 .with_width(100.0)
161 .with_height(20.0)
162 .with_name("Button"),
163 )
164 .build(&mut ui.build_ctx())
165 .into()
166 })
167 .with_group("Input")
168 }
169}
170
171crate::define_widget_deref!(Button);
172
173impl Control for Button {
174 fn update(&mut self, dt: f32, ui: &mut UserInterface) {
175 let mut repeat_timer = self.repeat_timer.borrow_mut();
176 if let Some(repeat_timer) = &mut *repeat_timer {
177 *repeat_timer -= dt;
178 if *repeat_timer <= 0.0 {
179 ui.send_message(ButtonMessage::click(
180 self.handle(),
181 MessageDirection::FromWidget,
182 ));
183 *repeat_timer = *self.repeat_interval;
184 }
185 }
186 }
187
188 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
189 self.widget.handle_routed_message(ui, message);
190
191 if let Some(msg) = message.data::<WidgetMessage>() {
192 if message.destination() == self.handle()
193 || self.has_descendant(message.destination(), ui)
194 {
195 match msg {
196 WidgetMessage::MouseDown { .. }
197 | WidgetMessage::TouchStarted { .. }
198 | WidgetMessage::TouchMoved { .. } => {
199 // The only way to avoid a `MouseLeave` message is by capturing the currently picked node.
200 // Capturing any other node will change the picked node and be considered leaving,
201 // which would affect the decorator.
202 ui.capture_mouse(message.destination());
203 message.set_handled(true);
204 if *self.repeat_clicks_on_hold {
205 self.repeat_timer.replace(Some(*self.repeat_interval));
206 }
207 }
208 WidgetMessage::MouseUp { .. } | WidgetMessage::TouchEnded { .. } => {
209 // Do the click only if the mouse is still within the button and the event hasn't been handled.
210 // The event might be handled if there is a child button within this button, as with the
211 // close button on a tab.
212 if self.screen_bounds().contains(ui.cursor_position()) && !message.handled()
213 {
214 ui.send_message(ButtonMessage::click(
215 self.handle(),
216 MessageDirection::FromWidget,
217 ));
218 }
219 ui.release_mouse_capture();
220 message.set_handled(true);
221 self.repeat_timer.replace(None);
222 }
223 WidgetMessage::KeyDown(key_code) => {
224 if !message.handled()
225 && (*key_code == KeyCode::Enter || *key_code == KeyCode::Space)
226 {
227 ui.send_message(ButtonMessage::click(
228 self.handle,
229 MessageDirection::FromWidget,
230 ));
231 message.set_handled(true);
232 }
233 }
234 _ => (),
235 }
236 }
237 } else if let Some(msg) = message.data::<ButtonMessage>() {
238 if message.destination() == self.handle() {
239 match msg {
240 ButtonMessage::Click => (),
241 ButtonMessage::Content(content) => {
242 if self.content.is_some() {
243 ui.send_message(WidgetMessage::remove(
244 *self.content,
245 MessageDirection::ToWidget,
246 ));
247 }
248 self.content
249 .set_value_and_mark_modified(content.build(&mut ui.build_ctx()));
250 ui.send_message(WidgetMessage::link(
251 *self.content,
252 MessageDirection::ToWidget,
253 *self.decorator,
254 ));
255 }
256 ButtonMessage::RepeatInterval(interval) => {
257 if *self.repeat_interval != *interval
258 && message.direction() == MessageDirection::ToWidget
259 {
260 *self.repeat_interval = *interval;
261 ui.send_message(message.reverse());
262 }
263 }
264 ButtonMessage::RepeatClicksOnHold(repeat_clicks) => {
265 if *self.repeat_clicks_on_hold != *repeat_clicks
266 && message.direction() == MessageDirection::ToWidget
267 {
268 *self.repeat_clicks_on_hold = *repeat_clicks;
269 ui.send_message(message.reverse());
270 }
271 }
272 }
273 }
274 }
275 }
276}
277
278/// Possible button content. In general, button widget can contain any type of widget inside. This enum contains
279/// a special shortcuts for most commonly used cases - button with the default font, button with custom font, or
280/// button with any widget.
281#[derive(Debug, Clone, PartialEq)]
282pub enum ButtonContent {
283 /// A shortcut to create a [crate::text::Text] widget as the button content. It is the same as creating Text
284 /// widget yourself, but much shorter.
285 Text {
286 /// Text of the button.
287 text: String,
288 /// Optional font of the button. If [`None`], the default font will be used.
289 font: Option<FontResource>,
290 /// Font size of the text. Default is 14.0 (defined by default style of the crate).
291 size: Option<StyledProperty<f32>>,
292 },
293 /// Arbitrary widget handle. It could be any widget handle, for example a handle of [`crate::image::Image`]
294 /// widget.
295 Node(Handle<UiNode>),
296}
297
298impl ButtonContent {
299 /// Creates [`ButtonContent::Text`] with default font.
300 pub fn text<S: AsRef<str>>(s: S) -> Self {
301 Self::Text {
302 text: s.as_ref().to_owned(),
303 font: None,
304 size: None,
305 }
306 }
307
308 /// Creates [`ButtonContent::Text`] with custom font.
309 pub fn text_with_font<S: AsRef<str>>(s: S, font: FontResource) -> Self {
310 Self::Text {
311 text: s.as_ref().to_owned(),
312 font: Some(font),
313 size: None,
314 }
315 }
316
317 /// Creates [`ButtonContent::Text`] with custom font and size.
318 pub fn text_with_font_size<S: AsRef<str>>(
319 s: S,
320 font: FontResource,
321 size: StyledProperty<f32>,
322 ) -> Self {
323 Self::Text {
324 text: s.as_ref().to_owned(),
325 font: Some(font),
326 size: Some(size),
327 }
328 }
329
330 /// Creates [`ButtonContent::Node`].
331 pub fn node(node: Handle<UiNode>) -> Self {
332 Self::Node(node)
333 }
334
335 fn build(&self, ctx: &mut BuildContext) -> Handle<UiNode> {
336 match self {
337 Self::Text { text, font, size } => TextBuilder::new(WidgetBuilder::new())
338 .with_text(text)
339 .with_horizontal_text_alignment(HorizontalAlignment::Center)
340 .with_vertical_text_alignment(VerticalAlignment::Center)
341 .with_font(font.clone().unwrap_or_else(|| ctx.default_font()))
342 .with_font_size(
343 size.clone()
344 .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
345 )
346 .build(ctx),
347 Self::Node(node) => *node,
348 }
349 }
350}
351
352/// Button builder is used to create [`Button`] widget instances.
353pub struct ButtonBuilder {
354 widget_builder: WidgetBuilder,
355 content: Option<ButtonContent>,
356 back: Option<Handle<UiNode>>,
357 repeat_interval: f32,
358 repeat_clicks_on_hold: bool,
359}
360
361impl ButtonBuilder {
362 /// Creates a new button builder with a widget builder instance.
363 pub fn new(widget_builder: WidgetBuilder) -> Self {
364 Self {
365 widget_builder,
366 content: None,
367 back: None,
368 repeat_interval: 0.1,
369 repeat_clicks_on_hold: false,
370 }
371 }
372
373 /// Sets the content of the button to be [`ButtonContent::Text`] (text with the default font).
374 pub fn with_text(mut self, text: &str) -> Self {
375 self.content = Some(ButtonContent::text(text));
376 self
377 }
378
379 /// Sets the content of the button to be [`ButtonContent::Text`] (text with a custom font).
380 pub fn with_text_and_font(mut self, text: &str, font: FontResource) -> Self {
381 self.content = Some(ButtonContent::text_with_font(text, font));
382 self
383 }
384
385 /// Sets the content of the button to be [`ButtonContent::Text`] (text with a custom font and size).
386 pub fn with_text_and_font_size(
387 mut self,
388 text: &str,
389 font: FontResource,
390 size: StyledProperty<f32>,
391 ) -> Self {
392 self.content = Some(ButtonContent::text_with_font_size(text, font, size));
393 self
394 }
395
396 /// Sets the content of the button to be [`ButtonContent::Node`] (arbitrary widget handle).
397 pub fn with_content(mut self, node: Handle<UiNode>) -> Self {
398 self.content = Some(ButtonContent::Node(node));
399 self
400 }
401
402 /// Specifies the widget that will be used as a content holder of the button. By default it is an
403 /// instance of [`crate::decorator::Decorator`] widget. Usually, this widget should respond to mouse
404 /// events to highlight button state (hovered, pressed, etc.)
405 pub fn with_back(mut self, decorator: Handle<UiNode>) -> Self {
406 self.back = Some(decorator);
407 self
408 }
409
410 /// Set the flag, that defines whether the button should repeat click message when being hold or not.
411 /// Default is `false` (disabled).
412 pub fn with_repeat_clicks_on_hold(mut self, repeat: bool) -> Self {
413 self.repeat_clicks_on_hold = repeat;
414 self
415 }
416
417 /// Sets the desired click repetition interval (in seconds) of the button. Default is 0.1s
418 pub fn with_repeat_interval(mut self, interval: f32) -> Self {
419 self.repeat_interval = interval;
420 self
421 }
422
423 /// Finishes building a button.
424 pub fn build_node(self, ctx: &mut BuildContext) -> UiNode {
425 let content = self.content.map(|c| c.build(ctx)).unwrap_or_default();
426 let back = self.back.unwrap_or_else(|| {
427 DecoratorBuilder::new(
428 BorderBuilder::new(
429 WidgetBuilder::new()
430 .with_foreground(ctx.style.property(Style::BRUSH_DARKER))
431 .with_child(content),
432 )
433 .with_pad_by_corner_radius(false)
434 .with_corner_radius(ctx.style.property(Button::CORNER_RADIUS))
435 .with_stroke_thickness(ctx.style.property(Button::BORDER_THICKNESS)),
436 )
437 .with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
438 .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
439 .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
440 .build(ctx)
441 });
442
443 if content.is_some() {
444 ctx.link(content, back);
445 }
446
447 UiNode::new(Button {
448 widget: self
449 .widget_builder
450 .with_accepts_input(true)
451 .with_need_update(true)
452 .with_child(back)
453 .build(ctx),
454 decorator: back.into(),
455 content: content.into(),
456 repeat_interval: self.repeat_interval.into(),
457 repeat_clicks_on_hold: self.repeat_clicks_on_hold.into(),
458 repeat_timer: Default::default(),
459 })
460 }
461
462 /// Finishes button build and adds to the user interface and returns its handle.
463 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
464 let node = self.build_node(ctx);
465 ctx.add_node(node)
466 }
467}
468
469#[cfg(test)]
470mod test {
471 use crate::button::ButtonBuilder;
472 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
473
474 #[test]
475 fn test_deletion() {
476 test_widget_deletion(|ctx| ButtonBuilder::new(WidgetBuilder::new()).build(ctx));
477 }
478}