1#![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#[derive(Debug, Clone, PartialEq)]
48pub enum ButtonMessage {
49 Click,
53 Content(ButtonContent),
55 RepeatInterval(f32),
58 RepeatClicksOnHold(bool),
60}
61impl MessageData for ButtonMessage {}
62
63#[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 pub widget: Widget,
102 pub decorator: InheritableVariable<Handle<UiNode>>,
104 pub content: InheritableVariable<Handle<UiNode>>,
106 #[visit(optional)]
108 #[reflect(min_value = 0.0)]
109 pub repeat_interval: InheritableVariable<f32>,
110 #[visit(optional)]
112 #[reflect(hidden)]
113 pub repeat_timer: RefCell<Option<f32>>,
114 #[visit(optional)]
117 pub repeat_clicks_on_hold: InheritableVariable<bool>,
118}
119
120impl Button {
121 pub const CORNER_RADIUS: &'static str = "Button.CornerRadius";
123 pub const BORDER_THICKNESS: &'static str = "Button.BorderThickness";
125
126 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 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 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#[derive(Debug, Clone, PartialEq)]
243pub enum ButtonContent {
244 Text {
247 text: String,
249 font: Option<FontResource>,
251 size: Option<StyledProperty<f32>>,
253 },
254 Node(Handle<UiNode>),
257}
258
259impl ButtonContent {
260 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 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 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 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
314pub 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 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 pub fn with_text(mut self, text: &str) -> Self {
346 self.content = Some(ButtonContent::text(text));
347 self
348 }
349
350 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 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 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 pub fn with_back(mut self, decorator: Handle<impl ObjectOrVariant<UiNode>>) -> Self {
377 self.back = Some(decorator.to_base());
378 self
379 }
380
381 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 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 pub fn with_repeat_clicks_on_hold(mut self, repeat: bool) -> Self {
406 self.repeat_clicks_on_hold = repeat;
407 self
408 }
409
410 pub fn with_repeat_interval(mut self, interval: f32) -> Self {
412 self.repeat_interval = interval;
413 self
414 }
415
416 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 pub fn build_node(self, ctx: &mut BuildContext) -> UiNode {
449 UiNode::new(self.build_button(ctx))
450 }
451
452 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}