Skip to main content

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