rg3d_ui/
scroll_bar.rs

1use crate::{
2    border::BorderBuilder,
3    brush::{Brush, GradientPoint},
4    button::{ButtonBuilder, ButtonMessage},
5    canvas::CanvasBuilder,
6    core::{
7        algebra::Vector2,
8        color::Color,
9        math::{self},
10        pool::Handle,
11    },
12    decorator::DecoratorBuilder,
13    define_constructor,
14    grid::{Column, GridBuilder, Row},
15    message::{MessageDirection, UiMessage},
16    text::{TextBuilder, TextMessage},
17    utils::{make_arrow, ArrowDirection},
18    widget::{Widget, WidgetBuilder, WidgetMessage},
19    BuildContext, Control, HorizontalAlignment, NodeHandleMapping, Orientation, Thickness, UiNode,
20    UserInterface, VerticalAlignment, BRUSH_LIGHT, BRUSH_LIGHTER, BRUSH_LIGHTEST, COLOR_DARKEST,
21    COLOR_LIGHTEST,
22};
23use std::{
24    any::{Any, TypeId},
25    ops::{Deref, DerefMut},
26};
27
28#[derive(Debug, Clone, PartialEq)]
29pub enum ScrollBarMessage {
30    Value(f32),
31    MinValue(f32),
32    MaxValue(f32),
33}
34
35impl ScrollBarMessage {
36    define_constructor!(ScrollBarMessage:Value => fn value(f32), layout: false);
37    define_constructor!(ScrollBarMessage:MaxValue => fn max_value(f32), layout: false);
38    define_constructor!(ScrollBarMessage:MinValue => fn min_value(f32), layout: false);
39}
40
41#[derive(Clone)]
42pub struct ScrollBar {
43    pub widget: Widget,
44    pub min: f32,
45    pub max: f32,
46    pub value: f32,
47    pub step: f32,
48    pub orientation: Orientation,
49    pub is_dragging: bool,
50    pub offset: Vector2<f32>,
51    pub increase: Handle<UiNode>,
52    pub decrease: Handle<UiNode>,
53    pub indicator: Handle<UiNode>,
54    pub field: Handle<UiNode>,
55    pub value_text: Handle<UiNode>,
56    pub value_precision: usize,
57}
58
59crate::define_widget_deref!(ScrollBar);
60
61impl Control for ScrollBar {
62    fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
63        if type_id == TypeId::of::<Self>() {
64            Some(self)
65        } else {
66            None
67        }
68    }
69
70    fn resolve(&mut self, node_map: &NodeHandleMapping) {
71        node_map.resolve(&mut self.increase);
72        node_map.resolve(&mut self.decrease);
73        node_map.resolve(&mut self.indicator);
74        if self.value_text.is_some() {
75            node_map.resolve(&mut self.value_text);
76        }
77        node_map.resolve(&mut self.field);
78    }
79
80    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
81        let size = self.widget.arrange_override(ui, final_size);
82
83        // Adjust indicator position according to current value
84        let percent = (self.value - self.min) / (self.max - self.min);
85
86        let field_size = ui.node(self.field).actual_size();
87
88        let indicator = ui.node(self.indicator);
89        match self.orientation {
90            Orientation::Horizontal => {
91                ui.send_message(WidgetMessage::height(
92                    self.indicator,
93                    MessageDirection::ToWidget,
94                    field_size.y,
95                ));
96                ui.send_message(WidgetMessage::width(
97                    self.decrease,
98                    MessageDirection::ToWidget,
99                    field_size.y,
100                ));
101                ui.send_message(WidgetMessage::width(
102                    self.increase,
103                    MessageDirection::ToWidget,
104                    field_size.y,
105                ));
106
107                let position = Vector2::new(
108                    percent * (field_size.x - indicator.actual_size().x).max(0.0),
109                    0.0,
110                );
111                ui.send_message(WidgetMessage::desired_position(
112                    self.indicator,
113                    MessageDirection::ToWidget,
114                    position,
115                ));
116            }
117            Orientation::Vertical => {
118                ui.send_message(WidgetMessage::width(
119                    self.indicator,
120                    MessageDirection::ToWidget,
121                    field_size.x,
122                ));
123                ui.send_message(WidgetMessage::height(
124                    self.decrease,
125                    MessageDirection::ToWidget,
126                    field_size.x,
127                ));
128                ui.send_message(WidgetMessage::height(
129                    self.increase,
130                    MessageDirection::ToWidget,
131                    field_size.x,
132                ));
133
134                let position = Vector2::new(
135                    0.0,
136                    percent * (field_size.y - indicator.actual_size().y).max(0.0),
137                );
138                ui.send_message(WidgetMessage::desired_position(
139                    self.indicator,
140                    MessageDirection::ToWidget,
141                    position,
142                ));
143            }
144        }
145
146        size
147    }
148
149    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
150        self.widget.handle_routed_message(ui, message);
151
152        if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
153            if message.destination() == self.increase {
154                ui.send_message(ScrollBarMessage::value(
155                    self.handle(),
156                    MessageDirection::ToWidget,
157                    self.value + self.step,
158                ));
159            } else if message.destination() == self.decrease {
160                ui.send_message(ScrollBarMessage::value(
161                    self.handle(),
162                    MessageDirection::ToWidget,
163                    self.value - self.step,
164                ));
165            }
166        } else if let Some(msg) = message.data::<ScrollBarMessage>() {
167            if message.destination() == self.handle()
168                && message.direction() == MessageDirection::ToWidget
169            {
170                match *msg {
171                    ScrollBarMessage::Value(value) => {
172                        let old_value = self.value;
173                        let new_value = math::clampf(value, self.min, self.max);
174                        if (new_value - old_value).abs() > f32::EPSILON {
175                            self.value = new_value;
176                            self.invalidate_arrange();
177
178                            if self.value_text.is_some() {
179                                ui.send_message(TextMessage::text(
180                                    self.value_text,
181                                    MessageDirection::ToWidget,
182                                    format!("{:.1$}", value, self.value_precision),
183                                ));
184                            }
185
186                            let response = ScrollBarMessage::value(
187                                self.handle,
188                                MessageDirection::FromWidget,
189                                self.value,
190                            );
191                            response.set_handled(message.handled());
192                            ui.send_message(response);
193                        }
194                    }
195                    ScrollBarMessage::MinValue(min) => {
196                        if self.min != min {
197                            self.min = min;
198                            if self.min > self.max {
199                                std::mem::swap(&mut self.min, &mut self.max);
200                            }
201                            let old_value = self.value;
202                            let new_value = math::clampf(self.value, self.min, self.max);
203                            if (new_value - old_value).abs() > f32::EPSILON {
204                                ui.send_message(ScrollBarMessage::value(
205                                    self.handle(),
206                                    MessageDirection::ToWidget,
207                                    new_value,
208                                ));
209                            }
210
211                            let response = ScrollBarMessage::min_value(
212                                self.handle,
213                                MessageDirection::FromWidget,
214                                self.min,
215                            );
216                            response.set_handled(message.handled());
217                            ui.send_message(response);
218                        }
219                    }
220                    ScrollBarMessage::MaxValue(max) => {
221                        if self.max != max {
222                            self.max = max;
223                            if self.max < self.min {
224                                std::mem::swap(&mut self.min, &mut self.max);
225                            }
226                            let old_value = self.value;
227                            let value = math::clampf(self.value, self.min, self.max);
228                            if (value - old_value).abs() > f32::EPSILON {
229                                ui.send_message(ScrollBarMessage::value(
230                                    self.handle(),
231                                    MessageDirection::ToWidget,
232                                    value,
233                                ));
234                            }
235
236                            let response = ScrollBarMessage::max_value(
237                                self.handle,
238                                MessageDirection::FromWidget,
239                                self.max,
240                            );
241                            response.set_handled(message.handled());
242                            ui.send_message(response);
243                        }
244                    }
245                }
246            }
247        } else if let Some(msg) = message.data::<WidgetMessage>() {
248            if message.destination() == self.indicator {
249                match msg {
250                    WidgetMessage::MouseDown { pos, .. } => {
251                        if self.indicator.is_some() {
252                            let indicator_pos = ui.nodes.borrow(self.indicator).screen_position;
253                            self.is_dragging = true;
254                            self.offset = indicator_pos - *pos;
255                            ui.capture_mouse(self.indicator);
256                            message.set_handled(true);
257                        }
258                    }
259                    WidgetMessage::MouseUp { .. } => {
260                        self.is_dragging = false;
261                        ui.release_mouse_capture();
262                        message.set_handled(true);
263                    }
264                    WidgetMessage::MouseMove { pos: mouse_pos, .. } => {
265                        if self.indicator.is_some() {
266                            let canvas =
267                                ui.borrow_by_name_up(self.indicator, ScrollBar::PART_CANVAS);
268                            let indicator_size = ui.nodes.borrow(self.indicator).actual_size();
269                            if self.is_dragging {
270                                let percent = match self.orientation {
271                                    Orientation::Horizontal => {
272                                        let span = canvas.actual_size().x - indicator_size.x;
273                                        let offset =
274                                            mouse_pos.x - canvas.screen_position.x + self.offset.x;
275                                        if span > 0.0 {
276                                            math::clampf(offset / span, 0.0, 1.0)
277                                        } else {
278                                            0.0
279                                        }
280                                    }
281                                    Orientation::Vertical => {
282                                        let span = canvas.actual_size().y - indicator_size.y;
283                                        let offset =
284                                            mouse_pos.y - canvas.screen_position.y + self.offset.y;
285                                        if span > 0.0 {
286                                            math::clampf(offset / span, 0.0, 1.0)
287                                        } else {
288                                            0.0
289                                        }
290                                    }
291                                };
292                                ui.send_message(ScrollBarMessage::value(
293                                    self.handle(),
294                                    MessageDirection::ToWidget,
295                                    self.min + percent * (self.max - self.min),
296                                ));
297                                message.set_handled(true);
298                            }
299                        }
300                    }
301                    _ => (),
302                }
303            }
304        }
305    }
306}
307
308impl ScrollBar {
309    pub const PART_CANVAS: &'static str = "PART_Canvas";
310
311    pub fn new(
312        widget: Widget,
313        increase: Handle<UiNode>,
314        decrease: Handle<UiNode>,
315        indicator: Handle<UiNode>,
316        field: Handle<UiNode>,
317        value_text: Handle<UiNode>,
318    ) -> Self {
319        Self {
320            widget,
321            min: 0.0,
322            max: 100.0,
323            value: 0.0,
324            step: 1.0,
325            orientation: Orientation::Vertical,
326            is_dragging: false,
327            offset: Default::default(),
328            increase,
329            decrease,
330            indicator,
331            field,
332            value_text,
333            value_precision: 3,
334        }
335    }
336
337    pub fn value(&self) -> f32 {
338        self.value
339    }
340
341    pub fn max_value(&self) -> f32 {
342        self.max
343    }
344
345    pub fn min_value(&self) -> f32 {
346        self.min
347    }
348
349    pub fn set_step(&mut self, step: f32) -> &mut Self {
350        self.step = step;
351        self
352    }
353
354    pub fn step(&self) -> f32 {
355        self.step
356    }
357}
358
359pub struct ScrollBarBuilder {
360    widget_builder: WidgetBuilder,
361    min: Option<f32>,
362    max: Option<f32>,
363    value: Option<f32>,
364    step: Option<f32>,
365    orientation: Option<Orientation>,
366    increase: Option<Handle<UiNode>>,
367    decrease: Option<Handle<UiNode>>,
368    indicator: Option<Handle<UiNode>>,
369    body: Option<Handle<UiNode>>,
370    show_value: bool,
371    value_precision: usize,
372}
373
374impl ScrollBarBuilder {
375    pub fn new(widget_builder: WidgetBuilder) -> Self {
376        Self {
377            widget_builder,
378            min: None,
379            max: None,
380            value: None,
381            step: None,
382            orientation: None,
383            increase: None,
384            decrease: None,
385            indicator: None,
386            body: None,
387            show_value: false,
388            value_precision: 3,
389        }
390    }
391
392    pub fn with_min(mut self, min: f32) -> Self {
393        self.min = Some(min);
394        self
395    }
396
397    pub fn with_max(mut self, max: f32) -> Self {
398        self.max = Some(max);
399        self
400    }
401
402    pub fn with_value(mut self, value: f32) -> Self {
403        self.value = Some(value);
404        self
405    }
406
407    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
408        self.orientation = Some(orientation);
409        self
410    }
411
412    pub fn with_step(mut self, step: f32) -> Self {
413        self.step = Some(step);
414        self
415    }
416
417    pub fn with_increase(mut self, increase: Handle<UiNode>) -> Self {
418        self.increase = Some(increase);
419        self
420    }
421
422    pub fn with_decrease(mut self, decrease: Handle<UiNode>) -> Self {
423        self.decrease = Some(decrease);
424        self
425    }
426
427    pub fn with_indicator(mut self, indicator: Handle<UiNode>) -> Self {
428        self.indicator = Some(indicator);
429        self
430    }
431
432    pub fn with_body(mut self, body: Handle<UiNode>) -> Self {
433        self.body = Some(body);
434        self
435    }
436
437    pub fn show_value(mut self, state: bool) -> Self {
438        self.show_value = state;
439        self
440    }
441
442    pub fn with_value_precision(mut self, precision: usize) -> Self {
443        self.value_precision = precision;
444        self
445    }
446
447    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
448        let orientation = self.orientation.unwrap_or(Orientation::Horizontal);
449
450        let increase = self.increase.unwrap_or_else(|| {
451            ButtonBuilder::new(WidgetBuilder::new())
452                .with_content(match orientation {
453                    Orientation::Horizontal => make_arrow(ctx, ArrowDirection::Right, 8.0),
454                    Orientation::Vertical => make_arrow(ctx, ArrowDirection::Bottom, 8.0),
455                })
456                .build(ctx)
457        });
458
459        match orientation {
460            Orientation::Vertical => {
461                ctx[increase].set_height(30.0).set_row(2).set_column(0);
462            }
463            Orientation::Horizontal => {
464                ctx[increase].set_width(30.0).set_row(0).set_column(2);
465            }
466        }
467
468        let decrease = self.decrease.unwrap_or_else(|| {
469            ButtonBuilder::new(WidgetBuilder::new())
470                .with_content(match orientation {
471                    Orientation::Horizontal => make_arrow(ctx, ArrowDirection::Left, 8.0),
472                    Orientation::Vertical => make_arrow(ctx, ArrowDirection::Top, 8.0),
473                })
474                .build(ctx)
475        });
476
477        ctx[decrease].set_row(0).set_column(0);
478
479        match orientation {
480            Orientation::Vertical => ctx[decrease].set_height(30.0),
481            Orientation::Horizontal => ctx[decrease].set_width(30.0),
482        };
483
484        let indicator = self.indicator.unwrap_or_else(|| {
485            DecoratorBuilder::new(
486                BorderBuilder::new(WidgetBuilder::new().with_foreground(Brush::LinearGradient {
487                    from: Vector2::new(0.5, 0.0),
488                    to: Vector2::new(0.5, 1.0),
489                    stops: vec![
490                        GradientPoint {
491                            stop: 0.0,
492                            color: COLOR_DARKEST,
493                        },
494                        GradientPoint {
495                            stop: 0.25,
496                            color: COLOR_LIGHTEST,
497                        },
498                        GradientPoint {
499                            stop: 0.75,
500                            color: COLOR_LIGHTEST,
501                        },
502                        GradientPoint {
503                            stop: 1.0,
504                            color: COLOR_DARKEST,
505                        },
506                    ],
507                }))
508                .with_stroke_thickness(Thickness::uniform(1.0)),
509            )
510            .with_normal_brush(BRUSH_LIGHT)
511            .with_hover_brush(BRUSH_LIGHTER)
512            .with_pressed_brush(BRUSH_LIGHTEST)
513            .build(ctx)
514        });
515
516        match orientation {
517            Orientation::Vertical => {
518                ctx[indicator]
519                    .set_min_size(Vector2::new(0.0, 30.0))
520                    .set_width(30.0);
521            }
522            Orientation::Horizontal => {
523                ctx[indicator]
524                    .set_min_size(Vector2::new(30.0, 0.0))
525                    .set_height(30.0);
526            }
527        }
528
529        let min = self.min.unwrap_or(0.0);
530        let max = self.max.unwrap_or(100.0);
531        let value = math::clampf(self.value.unwrap_or(0.0), min, max);
532
533        let value_text = if self.show_value {
534            let value_text = TextBuilder::new(
535                WidgetBuilder::new()
536                    .with_visibility(self.show_value)
537                    .with_horizontal_alignment(HorizontalAlignment::Center)
538                    .with_vertical_alignment(VerticalAlignment::Center)
539                    .with_hit_test_visibility(false)
540                    .with_margin(Thickness::uniform(3.0))
541                    .on_column(match orientation {
542                        Orientation::Horizontal => 1,
543                        Orientation::Vertical => 0,
544                    })
545                    .on_row(match orientation {
546                        Orientation::Horizontal => 0,
547                        Orientation::Vertical => 1,
548                    }),
549            )
550            .with_text(format!("{:.1$}", value, self.value_precision))
551            .build(ctx);
552
553            ctx.link(value_text, indicator);
554
555            value_text
556        } else {
557            Handle::NONE
558        };
559
560        let field = CanvasBuilder::new(
561            WidgetBuilder::new()
562                .with_name(ScrollBar::PART_CANVAS)
563                .on_column(match orientation {
564                    Orientation::Horizontal => 1,
565                    Orientation::Vertical => 0,
566                })
567                .on_row(match orientation {
568                    Orientation::Horizontal => 0,
569                    Orientation::Vertical => 1,
570                })
571                .with_child(indicator),
572        )
573        .build(ctx);
574
575        let grid = GridBuilder::new(
576            WidgetBuilder::new()
577                .with_child(decrease)
578                .with_child(field)
579                .with_child(increase),
580        )
581        .add_rows(match orientation {
582            Orientation::Horizontal => vec![Row::stretch()],
583            Orientation::Vertical => vec![Row::auto(), Row::stretch(), Row::auto()],
584        })
585        .add_columns(match orientation {
586            Orientation::Horizontal => vec![Column::auto(), Column::stretch(), Column::auto()],
587            Orientation::Vertical => vec![Column::stretch()],
588        })
589        .build(ctx);
590
591        let body = self.body.unwrap_or_else(|| {
592            BorderBuilder::new(
593                WidgetBuilder::new().with_background(Brush::Solid(Color::opaque(60, 60, 60))),
594            )
595            .with_stroke_thickness(Thickness::uniform(1.0))
596            .build(ctx)
597        });
598        ctx.link(grid, body);
599
600        let node = UiNode::new(ScrollBar {
601            widget: self.widget_builder.with_child(body).build(),
602            min,
603            max,
604            value,
605            step: self.step.unwrap_or(1.0),
606            orientation,
607            is_dragging: false,
608            offset: Vector2::default(),
609            increase,
610            decrease,
611            indicator,
612            field,
613            value_text,
614            value_precision: self.value_precision,
615        });
616        ctx.add_node(node)
617    }
618}