1#![warn(missing_docs)]
25
26use crate::button::Button;
27use crate::canvas::Canvas;
28use crate::text::Text;
29use crate::{
30 border::BorderBuilder,
31 brush::Brush,
32 button::{ButtonBuilder, ButtonMessage},
33 canvas::CanvasBuilder,
34 core::{
35 algebra::Vector2, color::Color, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
36 visitor::prelude::*,
37 },
38 decorator::DecoratorBuilder,
39 font::FontResource,
40 grid::{Column, GridBuilder, Row},
41 message::{MessageData, UiMessage},
42 style::{resource::StyleResourceExt, Style, StyledProperty},
43 text::{TextBuilder, TextMessage},
44 utils::{make_arrow, ArrowDirection},
45 widget::{Widget, WidgetBuilder, WidgetMessage},
46 BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
47 VerticalAlignment,
48};
49use fyrox_core::uuid_provider;
50use fyrox_core::variable::InheritableVariable;
51use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
52use fyrox_graph::SceneGraph;
53
54#[derive(Debug, Clone, PartialEq)]
56pub enum ScrollBarMessage {
57 Value(f32),
60 MinValue(f32),
63 MaxValue(f32),
66 SizeRatio(f32),
69}
70impl MessageData for ScrollBarMessage {}
71
72#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
134#[reflect(derived_type = "UiNode")]
135pub struct ScrollBar {
136 pub widget: Widget,
138 pub min: InheritableVariable<f32>,
140 pub max: InheritableVariable<f32>,
142 pub value: InheritableVariable<f32>,
144 pub step: InheritableVariable<f32>,
146 pub orientation: InheritableVariable<Orientation>,
148 pub is_dragging: bool,
150 pub offset: Vector2<f32>,
152 pub increase: InheritableVariable<Handle<Button>>,
154 pub decrease: InheritableVariable<Handle<Button>>,
156 pub indicator: InheritableVariable<Handle<UiNode>>,
158 pub indicator_canvas: InheritableVariable<Handle<Canvas>>,
160 pub value_text: InheritableVariable<Handle<Text>>,
162 pub value_precision: InheritableVariable<usize>,
164}
165
166impl ConstructorProvider<UiNode, UserInterface> for ScrollBar {
167 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
168 GraphNodeConstructor::new::<Self>()
169 .with_variant("Scroll Bar", |ui| {
170 ScrollBarBuilder::new(WidgetBuilder::new().with_name("Scroll Bar"))
171 .build(&mut ui.build_ctx())
172 .to_base()
173 .into()
174 })
175 .with_group("Input")
176 }
177}
178
179crate::define_widget_deref!(ScrollBar);
180
181uuid_provider!(ScrollBar = "92accc96-b334-424d-97ea-332c4787acf6");
182
183impl Control for ScrollBar {
184 fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
185 let size = self.widget.arrange_override(ui, final_size);
186
187 let percent = (*self.value - *self.min) / (*self.max - *self.min);
189
190 let field_size = ui[*self.indicator_canvas].actual_local_size();
191
192 let indicator = ui.node(*self.indicator);
193 match *self.orientation {
194 Orientation::Horizontal => {
195 ui.send(*self.indicator, WidgetMessage::Height(field_size.y));
196 ui.send(*self.decrease, WidgetMessage::Width(field_size.y));
197 ui.send(*self.increase, WidgetMessage::Width(field_size.y));
198
199 let position = Vector2::new(
200 percent * (field_size.x - indicator.actual_local_size().x).max(0.0),
201 0.0,
202 );
203 ui.send(*self.indicator, WidgetMessage::DesiredPosition(position));
204 }
205 Orientation::Vertical => {
206 ui.send(*self.indicator, WidgetMessage::Width(field_size.x));
207 ui.send(*self.decrease, WidgetMessage::Height(field_size.x));
208 ui.send(*self.increase, WidgetMessage::Height(field_size.x));
209
210 let position = Vector2::new(
211 0.0,
212 percent * (field_size.y - indicator.actual_local_size().y).max(0.0),
213 );
214 ui.send(*self.indicator, WidgetMessage::DesiredPosition(position));
215 }
216 }
217
218 size
219 }
220
221 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
222 self.widget.handle_routed_message(ui, message);
223
224 if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
225 if message.destination() == *self.increase {
226 ui.send(
227 self.handle(),
228 ScrollBarMessage::Value(*self.value + *self.step),
229 );
230 } else if message.destination() == *self.decrease {
231 ui.send(
232 self.handle(),
233 ScrollBarMessage::Value(*self.value - *self.step),
234 );
235 }
236 } else if let Some(msg) = message.data_for::<ScrollBarMessage>(self.handle()) {
237 match *msg {
238 ScrollBarMessage::Value(value) => {
239 let old_value = *self.value;
240 let new_value = value.clamp(*self.min, *self.max);
241 if (new_value - old_value).abs() > f32::EPSILON {
242 self.value.set_value_and_mark_modified(new_value);
243 self.invalidate_arrange();
244
245 if self.value_text.is_some() {
246 ui.send(
247 *self.value_text,
248 TextMessage::Text(format!("{:.1$}", value, *self.value_precision)),
249 );
250 }
251
252 let mut response = UiMessage::from_widget(
253 self.handle,
254 ScrollBarMessage::Value(*self.value),
255 );
256 response.flags = message.flags;
257 response.set_handled(message.handled());
258 ui.send_message(response);
259 }
260 }
261 ScrollBarMessage::MinValue(min) => {
262 if *self.min != min {
263 self.min.set_value_and_mark_modified(min);
264 if *self.min > *self.max {
265 std::mem::swap(&mut self.min, &mut self.max);
266 }
267 let old_value = *self.value;
268 let new_value = self.value.clamp(*self.min, *self.max);
269 if (new_value - old_value).abs() > f32::EPSILON {
270 ui.send(self.handle(), ScrollBarMessage::Value(new_value));
271 }
272
273 let response = UiMessage::from_widget(
274 self.handle,
275 ScrollBarMessage::MinValue(*self.min),
276 );
277 response.set_handled(message.handled());
278 ui.send_message(response);
279 }
280 }
281 ScrollBarMessage::MaxValue(max) => {
282 if *self.max != max {
283 self.max.set_value_and_mark_modified(max);
284 if *self.max < *self.min {
285 std::mem::swap(&mut self.min, &mut self.max);
286 }
287 let old_value = *self.value;
288 let value = self.value.clamp(*self.min, *self.max);
289 if (value - old_value).abs() > f32::EPSILON {
290 ui.send(self.handle(), ScrollBarMessage::Value(value));
291 }
292
293 let response = UiMessage::from_widget(
294 self.handle,
295 ScrollBarMessage::MaxValue(*self.max),
296 );
297 response.set_handled(message.handled());
298 ui.send_message(response);
299 }
300 }
301 ScrollBarMessage::SizeRatio(size_ratio) => {
302 let field_size = ui[*self.indicator_canvas].actual_global_size();
303 let indicator_size = ui.node(*self.indicator).actual_global_size();
304
305 match *self.orientation {
306 Orientation::Horizontal => {
307 let new_size = (size_ratio * field_size.x).max(15.0);
309 let old_size = indicator_size.x;
310
311 if new_size != old_size {
312 ui.send(*self.indicator, WidgetMessage::Width(new_size));
313 }
314 }
315 Orientation::Vertical => {
316 let new_size = (size_ratio * field_size.y).max(15.0);
318 let old_size = indicator_size.y;
319
320 if new_size != old_size {
321 ui.send(*self.indicator, WidgetMessage::Height(new_size));
322 }
323 }
324 }
325 }
326 }
327 } else if let Some(msg) = message.data::<WidgetMessage>() {
328 if message.destination() == *self.indicator {
329 match msg {
330 WidgetMessage::MouseDown { pos, .. } => {
331 if self.indicator.is_some() {
332 let indicator_pos = ui.nodes.borrow(*self.indicator).screen_position();
333 self.is_dragging = true;
334 self.offset = indicator_pos - *pos;
335 ui.capture_mouse(*self.indicator);
336 message.set_handled(true);
337 }
338 }
339 WidgetMessage::MouseUp { .. } => {
340 self.is_dragging = false;
341 ui.release_mouse_capture();
342 message.set_handled(true);
343 }
344 WidgetMessage::MouseMove { pos: mouse_pos, .. } => {
345 if self.indicator.is_some() {
346 let indicator_canvas = &ui[*self.indicator_canvas];
347 let indicator_size =
348 ui.nodes.borrow(*self.indicator).actual_global_size();
349 if self.is_dragging {
350 let percent = match *self.orientation {
351 Orientation::Horizontal => {
352 let span = indicator_canvas.actual_global_size().x
353 - indicator_size.x;
354 let offset = mouse_pos.x
355 - indicator_canvas.screen_position().x
356 + self.offset.x;
357 if span > 0.0 {
358 (offset / span).clamp(0.0, 1.0)
359 } else {
360 0.0
361 }
362 }
363 Orientation::Vertical => {
364 let span = indicator_canvas.actual_global_size().y
365 - indicator_size.y;
366 let offset = mouse_pos.y
367 - indicator_canvas.screen_position().y
368 + self.offset.y;
369 if span > 0.0 {
370 (offset / span).clamp(0.0, 1.0)
371 } else {
372 0.0
373 }
374 }
375 };
376 ui.send(
377 self.handle(),
378 ScrollBarMessage::Value(
379 *self.min + percent * (*self.max - *self.min),
380 ),
381 );
382 message.set_handled(true);
383 }
384 }
385 }
386 _ => (),
387 }
388 }
389 }
390 }
391}
392
393pub struct ScrollBarBuilder {
395 widget_builder: WidgetBuilder,
396 min: Option<f32>,
397 max: Option<f32>,
398 value: Option<f32>,
399 step: Option<f32>,
400 orientation: Option<Orientation>,
401 increase: Option<Handle<Button>>,
402 decrease: Option<Handle<Button>>,
403 indicator: Option<Handle<UiNode>>,
404 body: Option<Handle<UiNode>>,
405 show_value: bool,
406 value_precision: usize,
407 font: Option<FontResource>,
408 font_size: Option<StyledProperty<f32>>,
409}
410
411impl ScrollBarBuilder {
412 pub fn new(widget_builder: WidgetBuilder) -> Self {
414 Self {
415 widget_builder,
416 min: None,
417 max: None,
418 value: None,
419 step: None,
420 orientation: None,
421 increase: None,
422 decrease: None,
423 indicator: None,
424 body: None,
425 show_value: false,
426 value_precision: 3,
427 font: None,
428 font_size: None,
429 }
430 }
431
432 pub fn with_min(mut self, min: f32) -> Self {
434 self.min = Some(min);
435 self
436 }
437
438 pub fn with_max(mut self, max: f32) -> Self {
440 self.max = Some(max);
441 self
442 }
443
444 pub fn with_value(mut self, value: f32) -> Self {
446 self.value = Some(value);
447 self
448 }
449
450 pub fn with_orientation(mut self, orientation: Orientation) -> Self {
452 self.orientation = Some(orientation);
453 self
454 }
455
456 pub fn with_step(mut self, step: f32) -> Self {
458 self.step = Some(step);
459 self
460 }
461
462 pub fn with_increase(mut self, increase: Handle<Button>) -> Self {
464 self.increase = Some(increase);
465 self
466 }
467
468 pub fn with_decrease(mut self, decrease: Handle<Button>) -> Self {
470 self.decrease = Some(decrease);
471 self
472 }
473
474 pub fn with_indicator(mut self, indicator: Handle<UiNode>) -> Self {
476 self.indicator = Some(indicator);
477 self
478 }
479
480 pub fn with_body(mut self, body: Handle<UiNode>) -> Self {
482 self.body = Some(body);
483 self
484 }
485
486 pub fn show_value(mut self, state: bool) -> Self {
488 self.show_value = state;
489 self
490 }
491
492 pub fn with_value_precision(mut self, precision: usize) -> Self {
494 self.value_precision = precision;
495 self
496 }
497
498 pub fn with_font(mut self, font: FontResource) -> Self {
500 self.font = Some(font);
501 self
502 }
503
504 pub fn with_font_size(mut self, size: StyledProperty<f32>) -> Self {
506 self.font_size = Some(size);
507 self
508 }
509
510 pub fn build(self, ctx: &mut BuildContext) -> Handle<ScrollBar> {
512 let orientation = self.orientation.unwrap_or(Orientation::Horizontal);
513
514 let increase = self.increase.unwrap_or_else(|| {
515 ButtonBuilder::new(WidgetBuilder::new())
516 .with_content(match orientation {
517 Orientation::Horizontal => make_arrow(ctx, ArrowDirection::Right, 8.0),
518 Orientation::Vertical => make_arrow(ctx, ArrowDirection::Bottom, 8.0),
519 })
520 .with_repeat_clicks_on_hold(true)
521 .build(ctx)
522 });
523
524 match orientation {
525 Orientation::Vertical => {
526 ctx[increase].set_height(30.0).set_row(2).set_column(0);
527 }
528 Orientation::Horizontal => {
529 ctx[increase].set_width(30.0).set_row(0).set_column(2);
530 }
531 }
532
533 let decrease = self.decrease.unwrap_or_else(|| {
534 ButtonBuilder::new(WidgetBuilder::new())
535 .with_content(match orientation {
536 Orientation::Horizontal => make_arrow(ctx, ArrowDirection::Left, 8.0),
537 Orientation::Vertical => make_arrow(ctx, ArrowDirection::Top, 8.0),
538 })
539 .with_repeat_clicks_on_hold(true)
540 .build(ctx)
541 });
542
543 ctx[decrease].set_row(0).set_column(0);
544
545 match orientation {
546 Orientation::Vertical => ctx[decrease].set_height(30.0),
547 Orientation::Horizontal => ctx[decrease].set_width(30.0),
548 };
549
550 let indicator = self.indicator.unwrap_or_else(|| {
551 DecoratorBuilder::new(
552 BorderBuilder::new(
553 WidgetBuilder::new().with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
554 )
555 .with_corner_radius(8.0f32.into())
556 .with_pad_by_corner_radius(false)
557 .with_stroke_thickness(Thickness::uniform(1.0).into()),
558 )
559 .with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
560 .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
561 .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
562 .build(ctx)
563 .to_base()
564 });
565
566 match orientation {
567 Orientation::Vertical => {
568 ctx[indicator].set_min_size(Vector2::new(0.0, 15.0));
569 }
570 Orientation::Horizontal => {
571 ctx[indicator].set_min_size(Vector2::new(15.0, 0.0));
572 }
573 }
574
575 let min = self.min.unwrap_or(0.0);
576 let max = self.max.unwrap_or(100.0);
577 let value = self.value.unwrap_or(0.0).clamp(min, max);
578
579 let value_text = if self.show_value {
580 let value_text = TextBuilder::new(
581 WidgetBuilder::new()
582 .with_visibility(self.show_value)
583 .with_horizontal_alignment(HorizontalAlignment::Center)
584 .with_vertical_alignment(VerticalAlignment::Center)
585 .with_hit_test_visibility(false)
586 .with_margin(Thickness::uniform(3.0))
587 .on_column(match orientation {
588 Orientation::Horizontal => 1,
589 Orientation::Vertical => 0,
590 })
591 .on_row(match orientation {
592 Orientation::Horizontal => 0,
593 Orientation::Vertical => 1,
594 }),
595 )
596 .with_font(self.font.unwrap_or_else(|| ctx.default_font()))
597 .with_font_size(
598 self.font_size
599 .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
600 )
601 .with_text(format!("{:.1$}", value, self.value_precision))
602 .build(ctx);
603
604 ctx.link(value_text, indicator);
605
606 value_text
607 } else {
608 Handle::NONE
609 };
610
611 let indicator_canvas = CanvasBuilder::new(
612 WidgetBuilder::new()
613 .on_column(match orientation {
614 Orientation::Horizontal => 1,
615 Orientation::Vertical => 0,
616 })
617 .on_row(match orientation {
618 Orientation::Horizontal => 0,
619 Orientation::Vertical => 1,
620 })
621 .with_child(indicator),
622 )
623 .build(ctx);
624
625 let grid = GridBuilder::new(
626 WidgetBuilder::new()
627 .with_child(decrease)
628 .with_child(indicator_canvas)
629 .with_child(increase),
630 )
631 .add_rows(match orientation {
632 Orientation::Horizontal => vec![Row::stretch()],
633 Orientation::Vertical => vec![Row::auto(), Row::stretch(), Row::auto()],
634 })
635 .add_columns(match orientation {
636 Orientation::Horizontal => vec![Column::auto(), Column::stretch(), Column::auto()],
637 Orientation::Vertical => vec![Column::stretch()],
638 })
639 .build(ctx);
640
641 let body = self.body.unwrap_or_else(|| {
642 BorderBuilder::new(
643 WidgetBuilder::new().with_background(ctx.style.property(Style::BRUSH_DARK)),
644 )
645 .with_stroke_thickness(Thickness::uniform(1.0).into())
646 .build(ctx)
647 .to_base()
648 });
649 ctx.link(grid, body);
650
651 let node = ScrollBar {
652 widget: self.widget_builder.with_child(body).build(ctx),
653 min: min.into(),
654 max: max.into(),
655 value: value.into(),
656 step: self.step.unwrap_or(1.0).into(),
657 orientation: orientation.into(),
658 is_dragging: false,
659 offset: Vector2::default(),
660 increase: increase.into(),
661 decrease: decrease.into(),
662 indicator: indicator.into(),
663 indicator_canvas: indicator_canvas.into(),
664 value_text: value_text.into(),
665 value_precision: self.value_precision.into(),
666 };
667 ctx.add(node)
668 }
669}
670
671#[cfg(test)]
672mod test {
673 use crate::scroll_bar::ScrollBarBuilder;
674 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
675
676 #[test]
677 fn test_deletion() {
678 test_widget_deletion(|ctx| ScrollBarBuilder::new(WidgetBuilder::new()).build(ctx));
679 }
680}