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")]
118pub struct Button {
119    /// Base widget of the button.
120    pub widget: Widget,
121    /// Current content holder of the button.
122    pub decorator: InheritableVariable<Handle<UiNode>>,
123    /// Current content of the button. It is attached to the content holder.
124    pub content: InheritableVariable<Handle<UiNode>>,
125    /// Click repetition interval (in seconds) of the button.
126    #[visit(optional)]
127    #[reflect(min_value = 0.0)]
128    pub repeat_interval: InheritableVariable<f32>,
129    /// Current clicks repetition timer.
130    #[visit(optional)]
131    #[reflect(hidden)]
132    pub repeat_timer: RefCell<Option<f32>>,
133    /// A flag, that defines whether the button should repeat click message when being
134    /// hold or not. Default is `false` (disabled).
135    #[visit(optional)]
136    pub repeat_clicks_on_hold: InheritableVariable<bool>,
137}
138
139impl Button {
140    /// A name of style property, that defines corner radius of a button.
141    pub const CORNER_RADIUS: &'static str = "Button.CornerRadius";
142    /// A name of style property, that defines border thickness of a button.
143    pub const BORDER_THICKNESS: &'static str = "Button.BorderThickness";
144
145    /// Returns a style of the widget. This style contains only widget-specific properties.
146    pub fn style() -> Style {
147        Style::default()
148            .with(Self::CORNER_RADIUS, 4.0f32)
149            .with(Self::BORDER_THICKNESS, Thickness::uniform(1.0))
150    }
151}
152
153impl ConstructorProvider<UiNode, UserInterface> for Button {
154    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
155        GraphNodeConstructor::new::<Self>()
156            .with_variant("Button", |ui| {
157                ButtonBuilder::new(
158                    WidgetBuilder::new()
159                        .with_width(100.0)
160                        .with_height(20.0)
161                        .with_name("Button"),
162                )
163                .build(&mut ui.build_ctx())
164                .into()
165            })
166            .with_group("Input")
167    }
168}
169
170crate::define_widget_deref!(Button);
171
172impl Control for Button {
173    fn update(&mut self, dt: f32, ui: &mut UserInterface) {
174        let mut repeat_timer = self.repeat_timer.borrow_mut();
175        if let Some(repeat_timer) = &mut *repeat_timer {
176            *repeat_timer -= dt;
177            if *repeat_timer <= 0.0 {
178                ui.send_message(ButtonMessage::click(
179                    self.handle(),
180                    MessageDirection::FromWidget,
181                ));
182                *repeat_timer = *self.repeat_interval;
183            }
184        }
185    }
186
187    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
188        self.widget.handle_routed_message(ui, message);
189
190        if let Some(msg) = message.data::<WidgetMessage>() {
191            if message.destination() == self.handle()
192                || self.has_descendant(message.destination(), ui)
193            {
194                match msg {
195                    WidgetMessage::MouseDown { .. }
196                    | WidgetMessage::TouchStarted { .. }
197                    | WidgetMessage::TouchMoved { .. } => {
198                        ui.capture_mouse(self.handle);
199                        message.set_handled(true);
200                        if *self.repeat_clicks_on_hold {
201                            self.repeat_timer.replace(Some(*self.repeat_interval));
202                        }
203                    }
204                    WidgetMessage::MouseUp { .. } | WidgetMessage::TouchEnded { .. } => {
205                        ui.send_message(ButtonMessage::click(
206                            self.handle(),
207                            MessageDirection::FromWidget,
208                        ));
209                        ui.release_mouse_capture();
210                        message.set_handled(true);
211                        self.repeat_timer.replace(None);
212                    }
213                    WidgetMessage::KeyDown(key_code) => {
214                        if !message.handled()
215                            && (*key_code == KeyCode::Enter || *key_code == KeyCode::Space)
216                        {
217                            ui.send_message(ButtonMessage::click(
218                                self.handle,
219                                MessageDirection::FromWidget,
220                            ));
221                            message.set_handled(true);
222                        }
223                    }
224                    _ => (),
225                }
226            }
227        } else if let Some(msg) = message.data::<ButtonMessage>() {
228            if message.destination() == self.handle() {
229                match msg {
230                    ButtonMessage::Click => (),
231                    ButtonMessage::Content(content) => {
232                        if self.content.is_some() {
233                            ui.send_message(WidgetMessage::remove(
234                                *self.content,
235                                MessageDirection::ToWidget,
236                            ));
237                        }
238                        self.content
239                            .set_value_and_mark_modified(content.build(&mut ui.build_ctx()));
240                        ui.send_message(WidgetMessage::link(
241                            *self.content,
242                            MessageDirection::ToWidget,
243                            *self.decorator,
244                        ));
245                    }
246                    ButtonMessage::RepeatInterval(interval) => {
247                        if *self.repeat_interval != *interval
248                            && message.direction() == MessageDirection::ToWidget
249                        {
250                            *self.repeat_interval = *interval;
251                            ui.send_message(message.reverse());
252                        }
253                    }
254                    ButtonMessage::RepeatClicksOnHold(repeat_clicks) => {
255                        if *self.repeat_clicks_on_hold != *repeat_clicks
256                            && message.direction() == MessageDirection::ToWidget
257                        {
258                            *self.repeat_clicks_on_hold = *repeat_clicks;
259                            ui.send_message(message.reverse());
260                        }
261                    }
262                }
263            }
264        }
265    }
266}
267
268/// Possible button content. In general, button widget can contain any type of widget inside. This enum contains
269/// a special shortcuts for most commonly used cases - button with the default font, button with custom font, or
270/// button with any widget.
271#[derive(Debug, Clone, PartialEq)]
272pub enum ButtonContent {
273    /// A shortcut to create a [crate::text::Text] widget as the button content. It is the same as creating Text
274    /// widget yourself, but much shorter.
275    Text {
276        /// Text of the button.
277        text: String,
278        /// Optional font of the button. If [`None`], the default font will be used.
279        font: Option<FontResource>,
280        /// Font size of the text. Default is 14.0 (defined by default style of the crate).
281        size: Option<StyledProperty<f32>>,
282    },
283    /// Arbitrary widget handle. It could be any widget handle, for example a handle of [`crate::image::Image`]
284    /// widget.
285    Node(Handle<UiNode>),
286}
287
288impl ButtonContent {
289    /// Creates [`ButtonContent::Text`] with default font.
290    pub fn text<S: AsRef<str>>(s: S) -> Self {
291        Self::Text {
292            text: s.as_ref().to_owned(),
293            font: None,
294            size: None,
295        }
296    }
297
298    /// Creates [`ButtonContent::Text`] with custom font.
299    pub fn text_with_font<S: AsRef<str>>(s: S, font: FontResource) -> Self {
300        Self::Text {
301            text: s.as_ref().to_owned(),
302            font: Some(font),
303            size: None,
304        }
305    }
306
307    /// Creates [`ButtonContent::Text`] with custom font and size.
308    pub fn text_with_font_size<S: AsRef<str>>(
309        s: S,
310        font: FontResource,
311        size: StyledProperty<f32>,
312    ) -> Self {
313        Self::Text {
314            text: s.as_ref().to_owned(),
315            font: Some(font),
316            size: Some(size),
317        }
318    }
319
320    /// Creates [`ButtonContent::Node`].
321    pub fn node(node: Handle<UiNode>) -> Self {
322        Self::Node(node)
323    }
324
325    fn build(&self, ctx: &mut BuildContext) -> Handle<UiNode> {
326        match self {
327            Self::Text { text, font, size } => TextBuilder::new(WidgetBuilder::new())
328                .with_text(text)
329                .with_horizontal_text_alignment(HorizontalAlignment::Center)
330                .with_vertical_text_alignment(VerticalAlignment::Center)
331                .with_font(font.clone().unwrap_or_else(|| ctx.default_font()))
332                .with_font_size(
333                    size.clone()
334                        .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
335                )
336                .build(ctx),
337            Self::Node(node) => *node,
338        }
339    }
340}
341
342/// Button builder is used to create [`Button`] widget instances.
343pub struct ButtonBuilder {
344    widget_builder: WidgetBuilder,
345    content: Option<ButtonContent>,
346    back: Option<Handle<UiNode>>,
347    repeat_interval: f32,
348    repeat_clicks_on_hold: bool,
349}
350
351impl ButtonBuilder {
352    /// Creates a new button builder with a widget builder instance.
353    pub fn new(widget_builder: WidgetBuilder) -> Self {
354        Self {
355            widget_builder,
356            content: None,
357            back: None,
358            repeat_interval: 0.1,
359            repeat_clicks_on_hold: false,
360        }
361    }
362
363    /// Sets the content of the button to be [`ButtonContent::Text`] (text with the default font).
364    pub fn with_text(mut self, text: &str) -> Self {
365        self.content = Some(ButtonContent::text(text));
366        self
367    }
368
369    /// Sets the content of the button to be [`ButtonContent::Text`] (text with a custom font).
370    pub fn with_text_and_font(mut self, text: &str, font: FontResource) -> Self {
371        self.content = Some(ButtonContent::text_with_font(text, font));
372        self
373    }
374
375    /// Sets the content of the button to be [`ButtonContent::Text`] (text with a custom font and size).
376    pub fn with_text_and_font_size(
377        mut self,
378        text: &str,
379        font: FontResource,
380        size: StyledProperty<f32>,
381    ) -> Self {
382        self.content = Some(ButtonContent::text_with_font_size(text, font, size));
383        self
384    }
385
386    /// Sets the content of the button to be [`ButtonContent::Node`] (arbitrary widget handle).
387    pub fn with_content(mut self, node: Handle<UiNode>) -> Self {
388        self.content = Some(ButtonContent::Node(node));
389        self
390    }
391
392    /// Specifies the widget that will be used as a content holder of the button. By default it is an
393    /// instance of [`crate::decorator::Decorator`] widget. Usually, this widget should respond to mouse
394    /// events to highlight button state (hovered, pressed, etc.)
395    pub fn with_back(mut self, decorator: Handle<UiNode>) -> Self {
396        self.back = Some(decorator);
397        self
398    }
399
400    /// Set the flag, that defines whether the button should repeat click message when being hold or not.
401    /// Default is `false` (disabled).
402    pub fn with_repeat_clicks_on_hold(mut self, repeat: bool) -> Self {
403        self.repeat_clicks_on_hold = repeat;
404        self
405    }
406
407    /// Sets the desired click repetition interval (in seconds) of the button. Default is 0.1s
408    pub fn with_repeat_interval(mut self, interval: f32) -> Self {
409        self.repeat_interval = interval;
410        self
411    }
412
413    /// Finishes building a button.
414    pub fn build_node(self, ctx: &mut BuildContext) -> UiNode {
415        let content = self.content.map(|c| c.build(ctx)).unwrap_or_default();
416        let back = self.back.unwrap_or_else(|| {
417            DecoratorBuilder::new(
418                BorderBuilder::new(
419                    WidgetBuilder::new()
420                        .with_foreground(ctx.style.property(Style::BRUSH_DARKER))
421                        .with_child(content),
422                )
423                .with_pad_by_corner_radius(false)
424                .with_corner_radius(ctx.style.property(Button::CORNER_RADIUS))
425                .with_stroke_thickness(ctx.style.property(Button::BORDER_THICKNESS)),
426            )
427            .with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
428            .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
429            .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
430            .build(ctx)
431        });
432
433        if content.is_some() {
434            ctx.link(content, back);
435        }
436
437        UiNode::new(Button {
438            widget: self
439                .widget_builder
440                .with_accepts_input(true)
441                .with_need_update(true)
442                .with_child(back)
443                .build(ctx),
444            decorator: back.into(),
445            content: content.into(),
446            repeat_interval: self.repeat_interval.into(),
447            repeat_clicks_on_hold: self.repeat_clicks_on_hold.into(),
448            repeat_timer: Default::default(),
449        })
450    }
451
452    /// Finishes button build and adds to the user interface and returns its handle.
453    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
454        let node = self.build_node(ctx);
455        ctx.add_node(node)
456    }
457}
458
459#[cfg(test)]
460mod test {
461    use crate::button::ButtonBuilder;
462    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
463
464    #[test]
465    fn test_deletion() {
466        test_widget_deletion(|ctx| ButtonBuilder::new(WidgetBuilder::new()).build(ctx));
467    }
468}