1#![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#[derive(Debug, Clone, PartialEq)]
50pub enum ButtonMessage {
51 Click,
55 Content(ButtonContent),
57 RepeatInterval(f32),
60 RepeatClicksOnHold(bool),
62}
63
64impl ButtonMessage {
65 define_constructor!(
66 ButtonMessage:Click => fn click(), layout: false
68 );
69 define_constructor!(
70 ButtonMessage:Content => fn content(ButtonContent), layout: false
72 );
73 define_constructor!(
74 ButtonMessage:RepeatInterval => fn repeat_interval(f32), layout: false
76 );
77 define_constructor!(
78 ButtonMessage:RepeatClicksOnHold => fn repeat_clicks_on_hold(bool), layout: false
80 );
81}
82
83#[derive(Default, Clone, Visit, Reflect, Debug, TypeUuidProvider, ComponentProvider)]
117#[type_uuid(id = "2abcf12b-2f19-46da-b900-ae8890f7c9c6")]
118pub struct Button {
119 pub widget: Widget,
121 pub decorator: InheritableVariable<Handle<UiNode>>,
123 pub content: InheritableVariable<Handle<UiNode>>,
125 #[visit(optional)]
127 #[reflect(min_value = 0.0)]
128 pub repeat_interval: InheritableVariable<f32>,
129 #[visit(optional)]
131 #[reflect(hidden)]
132 pub repeat_timer: RefCell<Option<f32>>,
133 #[visit(optional)]
136 pub repeat_clicks_on_hold: InheritableVariable<bool>,
137}
138
139impl Button {
140 pub const CORNER_RADIUS: &'static str = "Button.CornerRadius";
142 pub const BORDER_THICKNESS: &'static str = "Button.BorderThickness";
144
145 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#[derive(Debug, Clone, PartialEq)]
272pub enum ButtonContent {
273 Text {
276 text: String,
278 font: Option<FontResource>,
280 size: Option<StyledProperty<f32>>,
282 },
283 Node(Handle<UiNode>),
286}
287
288impl ButtonContent {
289 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 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 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 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
342pub 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 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 pub fn with_text(mut self, text: &str) -> Self {
365 self.content = Some(ButtonContent::text(text));
366 self
367 }
368
369 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 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 pub fn with_content(mut self, node: Handle<UiNode>) -> Self {
388 self.content = Some(ButtonContent::Node(node));
389 self
390 }
391
392 pub fn with_back(mut self, decorator: Handle<UiNode>) -> Self {
396 self.back = Some(decorator);
397 self
398 }
399
400 pub fn with_repeat_clicks_on_hold(mut self, repeat: bool) -> Self {
403 self.repeat_clicks_on_hold = repeat;
404 self
405 }
406
407 pub fn with_repeat_interval(mut self, interval: f32) -> Self {
409 self.repeat_interval = interval;
410 self
411 }
412
413 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 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}