#![warn(missing_docs)]
use crate::message::MessageData;
use crate::style::StyledProperty;
use crate::{
border::BorderBuilder,
core::{
pool::Handle, reflect::prelude::*, type_traits::prelude::*, variable::InheritableVariable,
visitor::prelude::*,
},
decorator::DecoratorBuilder,
font::FontResource,
message::{KeyCode, UiMessage},
style::{resource::StyleResourceExt, Style},
text::TextBuilder,
widget::{Widget, WidgetBuilder, WidgetMessage},
BuildContext, Control, HorizontalAlignment, Thickness, UiNode, UserInterface,
VerticalAlignment,
};
use fyrox_core::pool::ObjectOrVariant;
use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
use std::cell::RefCell;
#[derive(Debug, Clone, PartialEq)]
pub enum ButtonMessage {
Click,
Content(ButtonContent),
RepeatInterval(f32),
RepeatClicksOnHold(bool),
}
impl MessageData for ButtonMessage {}
#[derive(Default, Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)]
#[type_uuid(id = "2abcf12b-2f19-46da-b900-ae8890f7c9c6")]
#[reflect(derived_type = "UiNode")]
pub struct Button {
pub widget: Widget,
pub decorator: InheritableVariable<Handle<UiNode>>,
pub content: InheritableVariable<Handle<UiNode>>,
#[visit(optional)]
#[reflect(min_value = 0.0)]
pub repeat_interval: InheritableVariable<f32>,
#[visit(optional)]
#[reflect(hidden)]
pub repeat_timer: RefCell<Option<f32>>,
#[visit(optional)]
pub repeat_clicks_on_hold: InheritableVariable<bool>,
}
impl Button {
pub const CORNER_RADIUS: &'static str = "Button.CornerRadius";
pub const BORDER_THICKNESS: &'static str = "Button.BorderThickness";
pub fn style() -> Style {
Style::default()
.with(Self::CORNER_RADIUS, 4.0f32)
.with(Self::BORDER_THICKNESS, Thickness::uniform(1.0))
}
}
impl ConstructorProvider<UiNode, UserInterface> for Button {
fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
GraphNodeConstructor::new::<Self>()
.with_variant("Button", |ui| {
ButtonBuilder::new(
WidgetBuilder::new()
.with_width(100.0)
.with_height(20.0)
.with_name("Button"),
)
.build(&mut ui.build_ctx())
.to_base()
.into()
})
.with_group("Input")
}
}
crate::define_widget_deref!(Button);
impl Control for Button {
fn update(&mut self, dt: f32, ui: &mut UserInterface) {
let mut repeat_timer = self.repeat_timer.borrow_mut();
if let Some(repeat_timer) = &mut *repeat_timer {
*repeat_timer -= dt;
if *repeat_timer <= 0.0 {
ui.post(self.handle(), ButtonMessage::Click);
*repeat_timer = *self.repeat_interval;
}
}
}
fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
self.widget.handle_routed_message(ui, message);
if let Some(msg) = message.data::<WidgetMessage>() {
if message.destination() == self.handle()
|| self.has_descendant(message.destination(), ui)
{
match msg {
WidgetMessage::MouseDown { .. }
| WidgetMessage::TouchStarted { .. }
| WidgetMessage::TouchMoved { .. } => {
ui.capture_mouse(message.destination());
message.set_handled(true);
if *self.repeat_clicks_on_hold {
self.repeat_timer.replace(Some(*self.repeat_interval));
}
}
WidgetMessage::MouseUp { .. } | WidgetMessage::TouchEnded { .. } => {
if self.screen_bounds().contains(ui.cursor_position()) && !message.handled()
{
ui.post(self.handle(), ButtonMessage::Click);
}
ui.release_mouse_capture();
message.set_handled(true);
self.repeat_timer.replace(None);
}
WidgetMessage::KeyDown(key_code) => {
if !message.handled()
&& (*key_code == KeyCode::Enter
|| *key_code == KeyCode::NumpadEnter
|| *key_code == KeyCode::Space)
{
ui.post(self.handle, ButtonMessage::Click);
message.set_handled(true);
}
}
_ => (),
}
}
} else if let Some(msg) = message.data_for::<ButtonMessage>(self.handle()) {
match msg {
ButtonMessage::Click => (),
ButtonMessage::Content(content) => {
if self.content.is_some() {
ui.send(*self.content, WidgetMessage::Remove);
}
self.content
.set_value_and_mark_modified(content.build(&mut ui.build_ctx()));
ui.send(*self.content, WidgetMessage::LinkWith(*self.decorator));
}
ButtonMessage::RepeatInterval(interval) => {
if *self.repeat_interval != *interval {
*self.repeat_interval = *interval;
ui.try_send_response(message);
}
}
ButtonMessage::RepeatClicksOnHold(repeat_clicks) => {
if *self.repeat_clicks_on_hold != *repeat_clicks {
*self.repeat_clicks_on_hold = *repeat_clicks;
ui.try_send_response(message);
}
}
}
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum ButtonContent {
Text {
text: String,
font: Option<FontResource>,
size: Option<StyledProperty<f32>>,
},
Node(Handle<UiNode>),
}
impl ButtonContent {
pub fn text<S: AsRef<str>>(s: S) -> Self {
Self::Text {
text: s.as_ref().to_owned(),
font: None,
size: None,
}
}
pub fn text_with_font<S: AsRef<str>>(s: S, font: FontResource) -> Self {
Self::Text {
text: s.as_ref().to_owned(),
font: Some(font),
size: None,
}
}
pub fn text_with_font_size<S: AsRef<str>>(
s: S,
font: FontResource,
size: StyledProperty<f32>,
) -> Self {
Self::Text {
text: s.as_ref().to_owned(),
font: Some(font),
size: Some(size),
}
}
pub fn node(node: Handle<UiNode>) -> Self {
Self::Node(node)
}
fn build(&self, ctx: &mut BuildContext) -> Handle<UiNode> {
match self {
Self::Text { text, font, size } => TextBuilder::new(WidgetBuilder::new())
.with_text(text)
.with_horizontal_text_alignment(HorizontalAlignment::Center)
.with_vertical_text_alignment(VerticalAlignment::Center)
.with_font(font.clone().unwrap_or_else(|| ctx.default_font()))
.with_font_size(
size.clone()
.unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
)
.build(ctx)
.to_base(),
Self::Node(node) => *node,
}
}
}
pub struct ButtonBuilder {
widget_builder: WidgetBuilder,
content: Option<ButtonContent>,
back: Option<Handle<UiNode>>,
repeat_interval: f32,
repeat_clicks_on_hold: bool,
}
fn make_decorator_builder(ctx: &mut BuildContext) -> DecoratorBuilder {
DecoratorBuilder::new(
BorderBuilder::new(WidgetBuilder::new())
.with_pad_by_corner_radius(false)
.with_corner_radius(ctx.style.property(Button::CORNER_RADIUS))
.with_stroke_thickness(ctx.style.property(Button::BORDER_THICKNESS)),
)
}
impl ButtonBuilder {
pub fn new(widget_builder: WidgetBuilder) -> Self {
Self {
widget_builder,
content: None,
back: None,
repeat_interval: 0.1,
repeat_clicks_on_hold: false,
}
}
pub fn with_text(mut self, text: &str) -> Self {
self.content = Some(ButtonContent::text(text));
self
}
pub fn with_text_and_font(mut self, text: &str, font: FontResource) -> Self {
self.content = Some(ButtonContent::text_with_font(text, font));
self
}
pub fn with_text_and_font_size(
mut self,
text: &str,
font: FontResource,
size: StyledProperty<f32>,
) -> Self {
self.content = Some(ButtonContent::text_with_font_size(text, font, size));
self
}
pub fn with_content(mut self, node: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
self.content = Some(ButtonContent::Node(node.to_base()));
self
}
pub fn with_back(mut self, decorator: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
self.back = Some(decorator.to_base());
self
}
pub fn with_ok_back(mut self, ctx: &mut BuildContext) -> Self {
self.back = Some(
make_decorator_builder(ctx)
.with_ok_style(ctx)
.build(ctx)
.to_base(),
);
self
}
pub fn with_cancel_back(mut self, ctx: &mut BuildContext) -> Self {
self.back = Some(
make_decorator_builder(ctx)
.with_cancel_style(ctx)
.build(ctx)
.to_base(),
);
self
}
pub fn with_repeat_clicks_on_hold(mut self, repeat: bool) -> Self {
self.repeat_clicks_on_hold = repeat;
self
}
pub fn with_repeat_interval(mut self, interval: f32) -> Self {
self.repeat_interval = interval;
self
}
pub fn build_button(self, ctx: &mut BuildContext) -> Button {
let content = self.content.map(|c| c.build(ctx)).unwrap_or_default();
let back = self.back.unwrap_or_else(|| {
make_decorator_builder(ctx)
.with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
.with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
.with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
.build(ctx)
.to_base()
});
if content.is_some() {
ctx.link(content, back);
}
Button {
widget: self
.widget_builder
.with_accepts_input(true)
.with_need_update(true)
.with_child(back)
.build(ctx),
decorator: back.into(),
content: content.into(),
repeat_interval: self.repeat_interval.into(),
repeat_clicks_on_hold: self.repeat_clicks_on_hold.into(),
repeat_timer: Default::default(),
}
}
pub fn build_node(self, ctx: &mut BuildContext) -> UiNode {
UiNode::new(self.build_button(ctx))
}
pub fn build(self, ctx: &mut BuildContext) -> Handle<Button> {
let node = self.build_button(ctx);
ctx.add(node)
}
}
#[cfg(test)]
mod test {
use crate::button::ButtonBuilder;
use crate::{test::test_widget_deletion, widget::WidgetBuilder};
#[test]
fn test_deletion() {
test_widget_deletion(|ctx| ButtonBuilder::new(WidgetBuilder::new()).build(ctx));
}
}