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.delivery_mode = message.delivery_mode;
258 response.set_handled(message.handled());
259 ui.send_message(response);
260 }
261 }
262 ScrollBarMessage::MinValue(min) => {
263 if *self.min != min {
264 self.min.set_value_and_mark_modified(min);
265 if *self.min > *self.max {
266 std::mem::swap(&mut self.min, &mut self.max);
267 }
268 let old_value = *self.value;
269 let new_value = self.value.clamp(*self.min, *self.max);
270 if (new_value - old_value).abs() > f32::EPSILON {
271 ui.send(self.handle(), ScrollBarMessage::Value(new_value));
272 }
273
274 let mut response = UiMessage::from_widget(
275 self.handle,
276 ScrollBarMessage::MinValue(*self.min),
277 );
278 response.flags = message.flags;
279 response.delivery_mode = message.delivery_mode;
280 response.set_handled(message.handled());
281 ui.send_message(response);
282 }
283 }
284 ScrollBarMessage::MaxValue(max) => {
285 if *self.max != max {
286 self.max.set_value_and_mark_modified(max);
287 if *self.max < *self.min {
288 std::mem::swap(&mut self.min, &mut self.max);
289 }
290 let old_value = *self.value;
291 let value = self.value.clamp(*self.min, *self.max);
292 if (value - old_value).abs() > f32::EPSILON {
293 ui.send(self.handle(), ScrollBarMessage::Value(value));
294 }
295
296 let mut response = UiMessage::from_widget(
297 self.handle,
298 ScrollBarMessage::MaxValue(*self.max),
299 );
300 response.flags = message.flags;
301 response.delivery_mode = message.delivery_mode;
302 response.set_handled(message.handled());
303 ui.send_message(response);
304 }
305 }
306 ScrollBarMessage::SizeRatio(size_ratio) => {
307 let field_size = ui[*self.indicator_canvas].actual_global_size();
308 let indicator_size = ui.node(*self.indicator).actual_global_size();
309
310 match *self.orientation {
311 Orientation::Horizontal => {
312 let new_size = (size_ratio * field_size.x).max(15.0);
314 let old_size = indicator_size.x;
315
316 if new_size != old_size {
317 ui.send(*self.indicator, WidgetMessage::Width(new_size));
318 }
319 }
320 Orientation::Vertical => {
321 let new_size = (size_ratio * field_size.y).max(15.0);
323 let old_size = indicator_size.y;
324
325 if new_size != old_size {
326 ui.send(*self.indicator, WidgetMessage::Height(new_size));
327 }
328 }
329 }
330 }
331 }
332 } else if let Some(msg) = message.data::<WidgetMessage>() {
333 if message.destination() == *self.indicator {
334 match msg {
335 WidgetMessage::MouseDown { pos, .. } => {
336 if self.indicator.is_some() {
337 let indicator_pos = ui.nodes.borrow(*self.indicator).screen_position();
338 self.is_dragging = true;
339 self.offset = indicator_pos - *pos;
340 ui.capture_mouse(*self.indicator);
341 message.set_handled(true);
342 }
343 }
344 WidgetMessage::MouseUp { .. } => {
345 self.is_dragging = false;
346 ui.release_mouse_capture();
347 message.set_handled(true);
348 }
349 WidgetMessage::MouseMove { pos: mouse_pos, .. } => {
350 if self.indicator.is_some() {
351 let indicator_canvas = &ui[*self.indicator_canvas];
352 let indicator_size =
353 ui.nodes.borrow(*self.indicator).actual_global_size();
354 if self.is_dragging {
355 let percent = match *self.orientation {
356 Orientation::Horizontal => {
357 let span = indicator_canvas.actual_global_size().x
358 - indicator_size.x;
359 let offset = mouse_pos.x
360 - indicator_canvas.screen_position().x
361 + self.offset.x;
362 if span > 0.0 {
363 (offset / span).clamp(0.0, 1.0)
364 } else {
365 0.0
366 }
367 }
368 Orientation::Vertical => {
369 let span = indicator_canvas.actual_global_size().y
370 - indicator_size.y;
371 let offset = mouse_pos.y
372 - indicator_canvas.screen_position().y
373 + self.offset.y;
374 if span > 0.0 {
375 (offset / span).clamp(0.0, 1.0)
376 } else {
377 0.0
378 }
379 }
380 };
381 ui.send(
382 self.handle(),
383 ScrollBarMessage::Value(
384 *self.min + percent * (*self.max - *self.min),
385 ),
386 );
387 message.set_handled(true);
388 }
389 }
390 }
391 _ => (),
392 }
393 }
394 }
395 }
396}
397
398pub struct ScrollBarBuilder {
400 widget_builder: WidgetBuilder,
401 min: Option<f32>,
402 max: Option<f32>,
403 value: Option<f32>,
404 step: Option<f32>,
405 orientation: Option<Orientation>,
406 increase: Option<Handle<Button>>,
407 decrease: Option<Handle<Button>>,
408 indicator: Option<Handle<UiNode>>,
409 body: Option<Handle<UiNode>>,
410 show_value: bool,
411 value_precision: usize,
412 font: Option<FontResource>,
413 font_size: Option<StyledProperty<f32>>,
414}
415
416impl ScrollBarBuilder {
417 pub fn new(widget_builder: WidgetBuilder) -> Self {
419 Self {
420 widget_builder,
421 min: None,
422 max: None,
423 value: None,
424 step: None,
425 orientation: None,
426 increase: None,
427 decrease: None,
428 indicator: None,
429 body: None,
430 show_value: false,
431 value_precision: 3,
432 font: None,
433 font_size: None,
434 }
435 }
436
437 pub fn with_min(mut self, min: f32) -> Self {
439 self.min = Some(min);
440 self
441 }
442
443 pub fn with_max(mut self, max: f32) -> Self {
445 self.max = Some(max);
446 self
447 }
448
449 pub fn with_value(mut self, value: f32) -> Self {
451 self.value = Some(value);
452 self
453 }
454
455 pub fn with_orientation(mut self, orientation: Orientation) -> Self {
457 self.orientation = Some(orientation);
458 self
459 }
460
461 pub fn with_step(mut self, step: f32) -> Self {
463 self.step = Some(step);
464 self
465 }
466
467 pub fn with_increase(mut self, increase: Handle<Button>) -> Self {
469 self.increase = Some(increase);
470 self
471 }
472
473 pub fn with_decrease(mut self, decrease: Handle<Button>) -> Self {
475 self.decrease = Some(decrease);
476 self
477 }
478
479 pub fn with_indicator(mut self, indicator: Handle<UiNode>) -> Self {
481 self.indicator = Some(indicator);
482 self
483 }
484
485 pub fn with_body(mut self, body: Handle<UiNode>) -> Self {
487 self.body = Some(body);
488 self
489 }
490
491 pub fn show_value(mut self, state: bool) -> Self {
493 self.show_value = state;
494 self
495 }
496
497 pub fn with_value_precision(mut self, precision: usize) -> Self {
499 self.value_precision = precision;
500 self
501 }
502
503 pub fn with_font(mut self, font: FontResource) -> Self {
505 self.font = Some(font);
506 self
507 }
508
509 pub fn with_font_size(mut self, size: StyledProperty<f32>) -> Self {
511 self.font_size = Some(size);
512 self
513 }
514
515 pub fn build(self, ctx: &mut BuildContext) -> Handle<ScrollBar> {
517 let orientation = self.orientation.unwrap_or(Orientation::Horizontal);
518
519 let increase = self.increase.unwrap_or_else(|| {
520 ButtonBuilder::new(WidgetBuilder::new())
521 .with_content(match orientation {
522 Orientation::Horizontal => make_arrow(ctx, ArrowDirection::Right, 8.0),
523 Orientation::Vertical => make_arrow(ctx, ArrowDirection::Bottom, 8.0),
524 })
525 .with_repeat_clicks_on_hold(true)
526 .build(ctx)
527 });
528
529 match orientation {
530 Orientation::Vertical => {
531 ctx[increase].set_height(30.0).set_row(2).set_column(0);
532 }
533 Orientation::Horizontal => {
534 ctx[increase].set_width(30.0).set_row(0).set_column(2);
535 }
536 }
537
538 let decrease = self.decrease.unwrap_or_else(|| {
539 ButtonBuilder::new(WidgetBuilder::new())
540 .with_content(match orientation {
541 Orientation::Horizontal => make_arrow(ctx, ArrowDirection::Left, 8.0),
542 Orientation::Vertical => make_arrow(ctx, ArrowDirection::Top, 8.0),
543 })
544 .with_repeat_clicks_on_hold(true)
545 .build(ctx)
546 });
547
548 ctx[decrease].set_row(0).set_column(0);
549
550 match orientation {
551 Orientation::Vertical => ctx[decrease].set_height(30.0),
552 Orientation::Horizontal => ctx[decrease].set_width(30.0),
553 };
554
555 let indicator = self.indicator.unwrap_or_else(|| {
556 DecoratorBuilder::new(
557 BorderBuilder::new(
558 WidgetBuilder::new().with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
559 )
560 .with_corner_radius(8.0f32.into())
561 .with_pad_by_corner_radius(false)
562 .with_stroke_thickness(Thickness::uniform(1.0).into()),
563 )
564 .with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
565 .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
566 .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
567 .build(ctx)
568 .to_base()
569 });
570
571 match orientation {
572 Orientation::Vertical => {
573 ctx[indicator].set_min_size(Vector2::new(0.0, 15.0));
574 }
575 Orientation::Horizontal => {
576 ctx[indicator].set_min_size(Vector2::new(15.0, 0.0));
577 }
578 }
579
580 let min = self.min.unwrap_or(0.0);
581 let max = self.max.unwrap_or(100.0);
582 let value = self.value.unwrap_or(0.0).clamp(min, max);
583
584 let value_text = if self.show_value {
585 let value_text = TextBuilder::new(
586 WidgetBuilder::new()
587 .with_visibility(self.show_value)
588 .with_horizontal_alignment(HorizontalAlignment::Center)
589 .with_vertical_alignment(VerticalAlignment::Center)
590 .with_hit_test_visibility(false)
591 .with_margin(Thickness::uniform(3.0))
592 .on_column(match orientation {
593 Orientation::Horizontal => 1,
594 Orientation::Vertical => 0,
595 })
596 .on_row(match orientation {
597 Orientation::Horizontal => 0,
598 Orientation::Vertical => 1,
599 }),
600 )
601 .with_font(self.font.unwrap_or_else(|| ctx.default_font()))
602 .with_font_size(
603 self.font_size
604 .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
605 )
606 .with_text(format!("{:.1$}", value, self.value_precision))
607 .build(ctx);
608
609 ctx.link(value_text, indicator);
610
611 value_text
612 } else {
613 Handle::NONE
614 };
615
616 let indicator_canvas = CanvasBuilder::new(
617 WidgetBuilder::new()
618 .on_column(match orientation {
619 Orientation::Horizontal => 1,
620 Orientation::Vertical => 0,
621 })
622 .on_row(match orientation {
623 Orientation::Horizontal => 0,
624 Orientation::Vertical => 1,
625 })
626 .with_child(indicator),
627 )
628 .build(ctx);
629
630 let grid = GridBuilder::new(
631 WidgetBuilder::new()
632 .with_child(decrease)
633 .with_child(indicator_canvas)
634 .with_child(increase),
635 )
636 .add_rows(match orientation {
637 Orientation::Horizontal => vec![Row::stretch()],
638 Orientation::Vertical => vec![Row::auto(), Row::stretch(), Row::auto()],
639 })
640 .add_columns(match orientation {
641 Orientation::Horizontal => vec![Column::auto(), Column::stretch(), Column::auto()],
642 Orientation::Vertical => vec![Column::stretch()],
643 })
644 .build(ctx);
645
646 let body = self.body.unwrap_or_else(|| {
647 BorderBuilder::new(
648 WidgetBuilder::new().with_background(ctx.style.property(Style::BRUSH_DARK)),
649 )
650 .with_stroke_thickness(Thickness::uniform(1.0).into())
651 .build(ctx)
652 .to_base()
653 });
654 ctx.link(grid, body);
655
656 let node = ScrollBar {
657 widget: self.widget_builder.with_child(body).build(ctx),
658 min: min.into(),
659 max: max.into(),
660 value: value.into(),
661 step: self.step.unwrap_or(1.0).into(),
662 orientation: orientation.into(),
663 is_dragging: false,
664 offset: Vector2::default(),
665 increase: increase.into(),
666 decrease: decrease.into(),
667 indicator: indicator.into(),
668 indicator_canvas: indicator_canvas.into(),
669 value_text: value_text.into(),
670 value_precision: self.value_precision.into(),
671 };
672 ctx.add(node)
673 }
674}
675
676#[cfg(test)]
677mod test {
678 use crate::scroll_bar::ScrollBarBuilder;
679 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
680
681 #[test]
682 fn test_deletion() {
683 test_widget_deletion(|ctx| ScrollBarBuilder::new(WidgetBuilder::new()).build(ctx));
684 }
685}