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 || *key_code == KeyCode::Space)
201                        {
202                            ui.post(self.handle, ButtonMessage::Click);
203                            message.set_handled(true);
204                        }
205                    }
206                    _ => (),
207                }
208            }
209        } else if let Some(msg) = message.data_for::<ButtonMessage>(self.handle()) {
210            match msg {
211                ButtonMessage::Click => (),
212                ButtonMessage::Content(content) => {
213                    if self.content.is_some() {
214                        ui.send(*self.content, WidgetMessage::Remove);
215                    }
216                    self.content
217                        .set_value_and_mark_modified(content.build(&mut ui.build_ctx()));
218                    ui.send(*self.content, WidgetMessage::LinkWith(*self.decorator));
219                }
220                ButtonMessage::RepeatInterval(interval) => {
221                    if *self.repeat_interval != *interval {
222                        *self.repeat_interval = *interval;
223                        ui.send_message(message.reverse());
224                    }
225                }
226                ButtonMessage::RepeatClicksOnHold(repeat_clicks) => {
227                    if *self.repeat_clicks_on_hold != *repeat_clicks {
228                        *self.repeat_clicks_on_hold = *repeat_clicks;
229                        ui.send_message(message.reverse());
230                    }
231                }
232            }
233        }
234    }
235}
236
237/// Possible button content. In general, button widget can contain any type of widget inside. This enum contains
238/// a special shortcuts for most commonly used cases - button with the default font, button with custom font, or
239/// button with any widget.
240#[derive(Debug, Clone, PartialEq)]
241pub enum ButtonContent {
242    /// A shortcut to create a [crate::text::Text] widget as the button content. It is the same as creating Text
243    /// widget yourself, but much shorter.
244    Text {
245        /// Text of the button.
246        text: String,
247        /// Optional font of the button. If [`None`], the default font will be used.
248        font: Option<FontResource>,
249        /// Font size of the text. Default is 14.0 (defined by default style of the crate).
250        size: Option<StyledProperty<f32>>,
251    },
252    /// Arbitrary widget handle. It could be any widget handle, for example, a handle of [`crate::image::Image`]
253    /// widget.
254    Node(Handle<UiNode>),
255}
256
257impl ButtonContent {
258    /// Creates [`ButtonContent::Text`] with default font.
259    pub fn text<S: AsRef<str>>(s: S) -> Self {
260        Self::Text {
261            text: s.as_ref().to_owned(),
262            font: None,
263            size: None,
264        }
265    }
266
267    /// Creates [`ButtonContent::Text`] with custom font.
268    pub fn text_with_font<S: AsRef<str>>(s: S, font: FontResource) -> Self {
269        Self::Text {
270            text: s.as_ref().to_owned(),
271            font: Some(font),
272            size: None,
273        }
274    }
275
276    /// Creates [`ButtonContent::Text`] with custom font and size.
277    pub fn text_with_font_size<S: AsRef<str>>(
278        s: S,
279        font: FontResource,
280        size: StyledProperty<f32>,
281    ) -> Self {
282        Self::Text {
283            text: s.as_ref().to_owned(),
284            font: Some(font),
285            size: Some(size),
286        }
287    }
288
289    /// Creates [`ButtonContent::Node`].
290    pub fn node(node: Handle<UiNode>) -> Self {
291        Self::Node(node)
292    }
293
294    fn build(&self, ctx: &mut BuildContext) -> Handle<UiNode> {
295        match self {
296            Self::Text { text, font, size } => TextBuilder::new(WidgetBuilder::new())
297                .with_text(text)
298                .with_horizontal_text_alignment(HorizontalAlignment::Center)
299                .with_vertical_text_alignment(VerticalAlignment::Center)
300                .with_font(font.clone().unwrap_or_else(|| ctx.default_font()))
301                .with_font_size(
302                    size.clone()
303                        .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
304                )
305                .build(ctx)
306                .to_base(),
307            Self::Node(node) => *node,
308        }
309    }
310}
311
312/// Button builder is used to create [`Button`] widget instances.
313pub struct ButtonBuilder {
314    widget_builder: WidgetBuilder,
315    content: Option<ButtonContent>,
316    back: Option<Handle<UiNode>>,
317    repeat_interval: f32,
318    repeat_clicks_on_hold: bool,
319}
320
321fn make_decorator_builder(ctx: &mut BuildContext) -> DecoratorBuilder {
322    DecoratorBuilder::new(
323        BorderBuilder::new(WidgetBuilder::new())
324            .with_pad_by_corner_radius(false)
325            .with_corner_radius(ctx.style.property(Button::CORNER_RADIUS))
326            .with_stroke_thickness(ctx.style.property(Button::BORDER_THICKNESS)),
327    )
328}
329
330impl ButtonBuilder {
331    /// Creates a new button builder with a widget builder instance.
332    pub fn new(widget_builder: WidgetBuilder) -> Self {
333        Self {
334            widget_builder,
335            content: None,
336            back: None,
337            repeat_interval: 0.1,
338            repeat_clicks_on_hold: false,
339        }
340    }
341
342    /// Sets the content of the button to be [`ButtonContent::Text`] (text with the default font).
343    pub fn with_text(mut self, text: &str) -> Self {
344        self.content = Some(ButtonContent::text(text));
345        self
346    }
347
348    /// Sets the content of the button to be [`ButtonContent::Text`] (text with a custom font).
349    pub fn with_text_and_font(mut self, text: &str, font: FontResource) -> Self {
350        self.content = Some(ButtonContent::text_with_font(text, font));
351        self
352    }
353
354    /// Sets the content of the button to be [`ButtonContent::Text`] (text with a custom font and size).
355    pub fn with_text_and_font_size(
356        mut self,
357        text: &str,
358        font: FontResource,
359        size: StyledProperty<f32>,
360    ) -> Self {
361        self.content = Some(ButtonContent::text_with_font_size(text, font, size));
362        self
363    }
364
365    /// Sets the content of the button to be [`ButtonContent::Node`] (arbitrary widget handle).
366    pub fn with_content(mut self, node: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
367        self.content = Some(ButtonContent::Node(node.to_base()));
368        self
369    }
370
371    /// Specifies the widget that will be used as a content holder of the button. By default, it is an
372    /// instance of [`crate::decorator::Decorator`] widget. Usually, this widget should respond to mouse
373    /// events to highlight button state (hovered, pressed, etc.)
374    pub fn with_back(mut self, decorator: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
375        self.back = Some(decorator.to_base());
376        self
377    }
378
379    /// Sets a new decorator background with `ok` style (green color by default).
380    pub fn with_ok_back(mut self, ctx: &mut BuildContext) -> Self {
381        self.back = Some(
382            make_decorator_builder(ctx)
383                .with_ok_style(ctx)
384                .build(ctx)
385                .to_base(),
386        );
387        self
388    }
389
390    /// Sets a new decorator background with `cancel` style (red color by default).
391    pub fn with_cancel_back(mut self, ctx: &mut BuildContext) -> Self {
392        self.back = Some(
393            make_decorator_builder(ctx)
394                .with_cancel_style(ctx)
395                .build(ctx)
396                .to_base(),
397        );
398        self
399    }
400
401    /// Set the flag, that defines whether the button should repeat click message when being hold or not.
402    /// Default is `false` (disabled).
403    pub fn with_repeat_clicks_on_hold(mut self, repeat: bool) -> Self {
404        self.repeat_clicks_on_hold = repeat;
405        self
406    }
407
408    /// Sets the desired click repetition interval (in seconds) of the button. Default is 0.1s
409    pub fn with_repeat_interval(mut self, interval: f32) -> Self {
410        self.repeat_interval = interval;
411        self
412    }
413
414    /// Finishes building a button.
415    pub fn build_button(self, ctx: &mut BuildContext) -> Button {
416        let content = self.content.map(|c| c.build(ctx)).unwrap_or_default();
417        let back = self.back.unwrap_or_else(|| {
418            make_decorator_builder(ctx)
419                .with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
420                .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
421                .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
422                .build(ctx)
423                .to_base()
424        });
425
426        if content.is_some() {
427            ctx.link(content, back);
428        }
429
430        Button {
431            widget: self
432                .widget_builder
433                .with_accepts_input(true)
434                .with_need_update(true)
435                .with_child(back)
436                .build(ctx),
437            decorator: back.into(),
438            content: content.into(),
439            repeat_interval: self.repeat_interval.into(),
440            repeat_clicks_on_hold: self.repeat_clicks_on_hold.into(),
441            repeat_timer: Default::default(),
442        }
443    }
444
445    /// Finishes building a button.
446    pub fn build_node(self, ctx: &mut BuildContext) -> UiNode {
447        UiNode::new(self.build_button(ctx))
448    }
449
450    /// Finishes button build and adds to the user interface and returns its handle.
451    pub fn build(self, ctx: &mut BuildContext) -> Handle<Button> {
452        let node = self.build_button(ctx);
453        ctx.add(node)
454    }
455}
456
457#[cfg(test)]
458mod test {
459    use crate::button::ButtonBuilder;
460    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
461
462    #[test]
463    fn test_deletion() {
464        test_widget_deletion(|ctx| ButtonBuilder::new(WidgetBuilder::new()).build(ctx));
465    }
466}