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}