Skip to main content

fyrox_ui/
scroll_bar.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Scroll bar is used to represent a value on a finite range. It has a thumb that shows the
22//! current value of the bar. See [`ScrollBar`] docs for more info and usage examples.
23
24#![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/// A set of messages that can be accepted by [`ScrollBar`] widget.
55#[derive(Debug, Clone, PartialEq)]
56pub enum ScrollBarMessage {
57    /// Used to indicate that the value of the scroll bar has changed ([`crate::message::MessageDirection::FromWidget`]) or to set a
58    /// new value (with [`crate::message::MessageDirection::ToWidget`].
59    Value(f32),
60    /// Used to indicate that the min value of the scroll bar has changed ([`crate::message::MessageDirection::FromWidget`]) or to set a
61    /// new min value (with [`crate::message::MessageDirection::ToWidget`].
62    MinValue(f32),
63    /// Used to indicate that the max value of the scroll bar has changed ([`crate::message::MessageDirection::FromWidget`]) or to set a
64    /// new max value (with [`crate::message::MessageDirection::ToWidget`].
65    MaxValue(f32),
66    /// Used to set the size of the indicator(thumb) adaptively, according to the relative sizes of the container and the
67    /// content in it (with [`crate::message::MessageDirection::ToWidget`].
68    SizeRatio(f32),
69}
70impl MessageData for ScrollBarMessage {}
71
72/// Scroll bar is used to represent a value on a finite range. It has a thumb that shows the current value
73/// on the bar. Usually it is used in pair with [`crate::scroll_panel::ScrollPanel`] to create something like
74/// [`crate::scroll_viewer::ScrollViewer`] widget. However, it could also be used to create sliders to show some
75/// value that lies within some range.
76///
77/// ## Example
78///
79/// A simple example of how to create a new [`ScrollBar`] could be something like this:
80///
81/// ```rust
82/// # use fyrox_ui::{
83/// #     core::pool::Handle, scroll_bar::ScrollBarBuilder, widget::WidgetBuilder, BuildContext,
84/// #     UiNode,
85/// # };
86/// # use fyrox_ui::scroll_bar::ScrollBar;
87///
88/// fn create_scroll_bar(ctx: &mut BuildContext) -> Handle<ScrollBar> {
89///     ScrollBarBuilder::new(WidgetBuilder::new())
90///         .with_min(0.0)
91///         .with_max(200.0)
92///         .with_value(123.0)
93///         .build(ctx)
94/// }
95/// ```
96///
97/// It creates a horizontal scroll bar with `123.0` value and a range of `[0.0..200.0]`. To fetch the new value
98/// of the scroll bar, use [`ScrollBarMessage::Value`] message:
99///
100/// ```rust
101/// # use fyrox_ui::{
102/// #     core::pool::Handle,
103/// #     message::{MessageDirection, UiMessage},
104/// #     scroll_bar::ScrollBarMessage,
105/// #     UiNode,
106/// # };
107/// # fn foo(scroll_bar: Handle<UiNode>, message: &mut UiMessage) {
108/// if let Some(ScrollBarMessage::Value(value)) = message.data_from(scroll_bar) {
109///     println!("{}", value);
110/// }
111/// # }
112/// ```
113///
114/// Please note, that you need to explicitly filter messages by [`crate::message::MessageDirection::FromWidget`], because it's the only
115/// direction that is used as an "indicator" that the value was accepted by the scroll bar.
116///
117/// ## Orientation
118///
119/// Scroll bar could be either horizontal (default) or vertical. You can select the orientation when building
120/// a scroll bar using [`ScrollBarBuilder::with_orientation`] method and provide a desired value from [`Orientation`]
121/// enum there.
122///
123/// ## Show values
124///
125/// By default, scroll bar does not show its actual value, you can turn it on using [`ScrollBarBuilder::show_value`]
126/// method with `true` as the first argument. To change rounding of the value, use [`ScrollBarBuilder::with_value_precision`]
127/// and provide the desired amount of decimal places there.
128///
129/// ## Step
130///
131/// Scroll bar provides arrows to change the current value using a fixed step value. You can change it using
132/// [`ScrollBarBuilder::with_step`] method.
133#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
134#[reflect(derived_type = "UiNode")]
135pub struct ScrollBar {
136    /// Base widget of the scroll bar.
137    pub widget: Widget,
138    /// Min value of the scroll bar.
139    pub min: InheritableVariable<f32>,
140    /// Max value of the scroll bar.
141    pub max: InheritableVariable<f32>,
142    /// Current value of the scroll bar.
143    pub value: InheritableVariable<f32>,
144    /// Step of the scroll bar.
145    pub step: InheritableVariable<f32>,
146    /// Current orientation of the scroll bar.
147    pub orientation: InheritableVariable<Orientation>,
148    /// Internal flag, that could be used to check whether the scroll bar's thumb is being dragged or not.
149    pub is_dragging: bool,
150    /// Internal mouse offset that is used for dragging purposes.
151    pub offset: Vector2<f32>,
152    /// A handle of the increase button.
153    pub increase: InheritableVariable<Handle<Button>>,
154    /// A handle of the decrease button.
155    pub decrease: InheritableVariable<Handle<Button>>,
156    /// A handle of the indicator (thumb).
157    pub indicator: InheritableVariable<Handle<UiNode>>,
158    /// A handle of the canvas that is used for the thumb.
159    pub indicator_canvas: InheritableVariable<Handle<Canvas>>,
160    /// A handle of the [`Text`] widget that is used to show the current value of the scroll bar.
161    pub value_text: InheritableVariable<Handle<Text>>,
162    /// Current value precision in decimal places.
163    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        // Adjust indicator position according to current value
188        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                            // the minimum size of the indicator will be 15 irrespective of size ratio
308                            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                            // the minimum size of the indicator will be 15 irrespective of size ratio
317                            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
393/// Scroll bar widget is used to create [`ScrollBar`] widget instances and add them to the user interface.
394pub 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    /// Creates new scroll bar builder instance.
413    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    /// Sets the desired min value.
433    pub fn with_min(mut self, min: f32) -> Self {
434        self.min = Some(min);
435        self
436    }
437
438    /// Sets the desired max value.
439    pub fn with_max(mut self, max: f32) -> Self {
440        self.max = Some(max);
441        self
442    }
443
444    /// Sets the desired value.
445    pub fn with_value(mut self, value: f32) -> Self {
446        self.value = Some(value);
447        self
448    }
449
450    /// Sets the desired orientation.
451    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
452        self.orientation = Some(orientation);
453        self
454    }
455
456    /// Sets the desired step.
457    pub fn with_step(mut self, step: f32) -> Self {
458        self.step = Some(step);
459        self
460    }
461
462    /// Sets the new handle to a button, that is used to increase the values of the scroll bar.
463    pub fn with_increase(mut self, increase: Handle<Button>) -> Self {
464        self.increase = Some(increase);
465        self
466    }
467
468    /// Sets the new handle to a button, that is used to decrease values of the scroll bar.
469    pub fn with_decrease(mut self, decrease: Handle<Button>) -> Self {
470        self.decrease = Some(decrease);
471        self
472    }
473
474    /// Sets the new handle to a widget, that is used as a thumb of the scroll bar.
475    pub fn with_indicator(mut self, indicator: Handle<UiNode>) -> Self {
476        self.indicator = Some(indicator);
477        self
478    }
479
480    /// Sets the new handle to a widget, that is used as a background of the scroll bar.
481    pub fn with_body(mut self, body: Handle<UiNode>) -> Self {
482        self.body = Some(body);
483        self
484    }
485
486    /// Show or hide the value of the scroll bar.
487    pub fn show_value(mut self, state: bool) -> Self {
488        self.show_value = state;
489        self
490    }
491
492    /// Sets the desired value precision of the scroll bar.
493    pub fn with_value_precision(mut self, precision: usize) -> Self {
494        self.value_precision = precision;
495        self
496    }
497
498    /// Sets the desired font.
499    pub fn with_font(mut self, font: FontResource) -> Self {
500        self.font = Some(font);
501        self
502    }
503
504    /// Sets the desired font size.
505    pub fn with_font_size(mut self, size: StyledProperty<f32>) -> Self {
506        self.font_size = Some(size);
507        self
508    }
509
510    /// Creates new scroll bar instance and adds it to the user interface.
511    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}