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 current value on
22//! on the bar. See [`ScrollBar`] docs for more info and usage examples.
23
24#![warn(missing_docs)]
25
26use crate::font::FontResource;
27use crate::style::resource::StyleResourceExt;
28use crate::style::{Style, StyledProperty};
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    define_constructor,
40    grid::{Column, GridBuilder, Row},
41    message::{MessageDirection, UiMessage},
42    text::{TextBuilder, TextMessage},
43    utils::{make_arrow, ArrowDirection},
44    widget::{Widget, WidgetBuilder, WidgetMessage},
45    BuildContext, Control, HorizontalAlignment, Orientation, Thickness, UiNode, UserInterface,
46    VerticalAlignment,
47};
48
49use fyrox_core::uuid_provider;
50use fyrox_core::variable::InheritableVariable;
51use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
52use fyrox_graph::BaseSceneGraph;
53use std::ops::{Deref, DerefMut};
54
55/// A set of messages that can be accepted by [`ScrollBar`] widget.
56#[derive(Debug, Clone, PartialEq)]
57pub enum ScrollBarMessage {
58    /// Used to indicate that the value of the scroll bar has changed ([`MessageDirection::FromWidget`]) or to set a
59    /// new value (with [`MessageDirection::ToWidget`].
60    Value(f32),
61    /// Used to indicate that the min value of the scroll bar has changed ([`MessageDirection::FromWidget`]) or to set a
62    /// new min value (with [`MessageDirection::ToWidget`].
63    MinValue(f32),
64    /// Used to indicate that the max value of the scroll bar has changed ([`MessageDirection::FromWidget`]) or to set a
65    /// new max value (with [`MessageDirection::ToWidget`].
66    MaxValue(f32),
67    /// Used to set the size of the indicator(thumb) adaptively, according to the relative sizes of the container and the
68    /// content in it (with [`MessageDirection::ToWidget`].
69    SizeRatio(f32),
70}
71
72impl ScrollBarMessage {
73    define_constructor!(
74        /// Creates [`ScrollBarMessage::Value`] message.
75        ScrollBarMessage:Value => fn value(f32), layout: false
76    );
77    define_constructor!(
78        /// Creates [`ScrollBarMessage::MaxValue`] message.
79        ScrollBarMessage:MaxValue => fn max_value(f32), layout: false
80    );
81    define_constructor!(
82        /// Creates [`ScrollBarMessage::MinValue`] message.
83        ScrollBarMessage:MinValue => fn min_value(f32), layout: false
84    );
85    define_constructor!(
86        /// Creates [`ScrollBarMessage::SizeRatio`] message.
87        ScrollBarMessage:SizeRatio => fn size_ratio(f32), layout: false
88    );
89}
90
91/// Scroll bar is used to represent a value on a finite range. It has a thumb that shows the current value on
92/// on the bar. Usually it is used in pair with [`crate::scroll_panel::ScrollPanel`] to create something like
93/// [`crate::scroll_viewer::ScrollViewer`] widget. However, it could also be used to create sliders to show some
94/// value that lies within some range.
95///
96/// ## Example
97///
98/// A simple example of how to create a new [`ScrollBar`] could be something like this:
99///
100/// ```rust
101/// # use fyrox_ui::{
102/// #     core::pool::Handle, scroll_bar::ScrollBarBuilder, widget::WidgetBuilder, BuildContext,
103/// #     UiNode,
104/// # };
105/// fn create_scroll_bar(ctx: &mut BuildContext) -> Handle<UiNode> {
106///     ScrollBarBuilder::new(WidgetBuilder::new())
107///         .with_min(0.0)
108///         .with_max(200.0)
109///         .with_value(123.0)
110///         .build(ctx)
111/// }
112/// ```
113///
114/// It creates a horizontal scroll bar with `123.0` value and a range of `[0.0..200.0]`. To fetch the new value
115/// of the scroll bar, use [`ScrollBarMessage::Value`] message:
116///
117/// ```rust
118/// # use fyrox_ui::{
119/// #     core::pool::Handle,
120/// #     message::{MessageDirection, UiMessage},
121/// #     scroll_bar::ScrollBarMessage,
122/// #     UiNode,
123/// # };
124/// # fn foo(scroll_bar: Handle<UiNode>, message: &mut UiMessage) {
125/// if message.destination() == scroll_bar
126///     && message.direction() == MessageDirection::FromWidget
127/// {
128///     if let Some(ScrollBarMessage::Value(value)) = message.data() {
129///         println!("{}", value);
130///     }
131/// }
132/// # }
133/// ```
134///
135/// Please note, that you need to explicitly filter messages by [`MessageDirection::FromWidget`], because it's the only
136/// direction that is used as an "indicator" that the value was accepted by the scroll bar.
137///
138/// ## Orientation
139///
140/// Scroll bar could be either horizontal (default) or vertical. You can select the orientation when building
141/// a scroll bar using [`ScrollBarBuilder::with_orientation`] method and provide a desired value from [`Orientation`]
142/// enum there.
143///
144/// ## Show values
145///
146/// By default, scroll bar does not show its actual value, you can turn it on using [`ScrollBarBuilder::show_value`]
147/// method with `true` as the first argument. To change rounding of the value, use [`ScrollBarBuilder::with_value_precision`]
148/// and provide the desired amount of decimal places there.
149///
150/// ## Step
151///
152/// Scroll bar provides arrows to change the current value using a fixed step value. You can change it using
153/// [`ScrollBarBuilder::with_step`] method.
154#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
155#[reflect(derived_type = "UiNode")]
156pub struct ScrollBar {
157    /// Base widget of the scroll bar.
158    pub widget: Widget,
159    /// Min value of the scroll bar.
160    pub min: InheritableVariable<f32>,
161    /// Max value of the scroll bar.
162    pub max: InheritableVariable<f32>,
163    /// Current value of the scroll bar.
164    pub value: InheritableVariable<f32>,
165    /// Step of the scroll bar.
166    pub step: InheritableVariable<f32>,
167    /// Current orientation of the scroll bar.
168    pub orientation: InheritableVariable<Orientation>,
169    /// Internal flag, that could be used to check whether the scroll bar's thumb is being dragged or not.
170    pub is_dragging: bool,
171    /// Internal mouse offset that is used for dragging purposes.
172    pub offset: Vector2<f32>,
173    /// A handle of the increase button.
174    pub increase: InheritableVariable<Handle<UiNode>>,
175    /// A handle of the decrease button.
176    pub decrease: InheritableVariable<Handle<UiNode>>,
177    /// A handle of the indicator (thumb).
178    pub indicator: InheritableVariable<Handle<UiNode>>,
179    /// A handle of the canvas that is used for the thumb.
180    pub indicator_canvas: InheritableVariable<Handle<UiNode>>,
181    /// A handle of the [`crate::text::Text`] widget that is used to show the current value of the scroll bar.
182    pub value_text: InheritableVariable<Handle<UiNode>>,
183    /// Current value precision in decimal places.
184    pub value_precision: InheritableVariable<usize>,
185}
186
187impl ConstructorProvider<UiNode, UserInterface> for ScrollBar {
188    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
189        GraphNodeConstructor::new::<Self>()
190            .with_variant("Scroll Bar", |ui| {
191                ScrollBarBuilder::new(WidgetBuilder::new().with_name("Scroll Bar"))
192                    .build(&mut ui.build_ctx())
193                    .into()
194            })
195            .with_group("Input")
196    }
197}
198
199crate::define_widget_deref!(ScrollBar);
200
201uuid_provider!(ScrollBar = "92accc96-b334-424d-97ea-332c4787acf6");
202
203impl Control for ScrollBar {
204    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
205        let size = self.widget.arrange_override(ui, final_size);
206
207        // Adjust indicator position according to current value
208        let percent = (*self.value - *self.min) / (*self.max - *self.min);
209
210        let field_size = ui.node(*self.indicator_canvas).actual_local_size();
211
212        let indicator = ui.node(*self.indicator);
213        match *self.orientation {
214            Orientation::Horizontal => {
215                ui.send_message(WidgetMessage::height(
216                    *self.indicator,
217                    MessageDirection::ToWidget,
218                    field_size.y,
219                ));
220                ui.send_message(WidgetMessage::width(
221                    *self.decrease,
222                    MessageDirection::ToWidget,
223                    field_size.y,
224                ));
225                ui.send_message(WidgetMessage::width(
226                    *self.increase,
227                    MessageDirection::ToWidget,
228                    field_size.y,
229                ));
230
231                let position = Vector2::new(
232                    percent * (field_size.x - indicator.actual_local_size().x).max(0.0),
233                    0.0,
234                );
235                ui.send_message(WidgetMessage::desired_position(
236                    *self.indicator,
237                    MessageDirection::ToWidget,
238                    position,
239                ));
240            }
241            Orientation::Vertical => {
242                ui.send_message(WidgetMessage::width(
243                    *self.indicator,
244                    MessageDirection::ToWidget,
245                    field_size.x,
246                ));
247                ui.send_message(WidgetMessage::height(
248                    *self.decrease,
249                    MessageDirection::ToWidget,
250                    field_size.x,
251                ));
252                ui.send_message(WidgetMessage::height(
253                    *self.increase,
254                    MessageDirection::ToWidget,
255                    field_size.x,
256                ));
257
258                let position = Vector2::new(
259                    0.0,
260                    percent * (field_size.y - indicator.actual_local_size().y).max(0.0),
261                );
262                ui.send_message(WidgetMessage::desired_position(
263                    *self.indicator,
264                    MessageDirection::ToWidget,
265                    position,
266                ));
267            }
268        }
269
270        size
271    }
272
273    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
274        self.widget.handle_routed_message(ui, message);
275
276        if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
277            if message.destination() == *self.increase {
278                ui.send_message(ScrollBarMessage::value(
279                    self.handle(),
280                    MessageDirection::ToWidget,
281                    *self.value + *self.step,
282                ));
283            } else if message.destination() == *self.decrease {
284                ui.send_message(ScrollBarMessage::value(
285                    self.handle(),
286                    MessageDirection::ToWidget,
287                    *self.value - *self.step,
288                ));
289            }
290        } else if let Some(msg) = message.data::<ScrollBarMessage>() {
291            if message.destination() == self.handle()
292                && message.direction() == MessageDirection::ToWidget
293            {
294                match *msg {
295                    ScrollBarMessage::Value(value) => {
296                        let old_value = *self.value;
297                        let new_value = value.clamp(*self.min, *self.max);
298                        if (new_value - old_value).abs() > f32::EPSILON {
299                            self.value.set_value_and_mark_modified(new_value);
300                            self.invalidate_arrange();
301
302                            if self.value_text.is_some() {
303                                ui.send_message(TextMessage::text(
304                                    *self.value_text,
305                                    MessageDirection::ToWidget,
306                                    format!("{:.1$}", value, *self.value_precision),
307                                ));
308                            }
309
310                            let mut response = ScrollBarMessage::value(
311                                self.handle,
312                                MessageDirection::FromWidget,
313                                *self.value,
314                            );
315                            response.flags = message.flags;
316                            response.set_handled(message.handled());
317                            ui.send_message(response);
318                        }
319                    }
320                    ScrollBarMessage::MinValue(min) => {
321                        if *self.min != min {
322                            self.min.set_value_and_mark_modified(min);
323                            if *self.min > *self.max {
324                                std::mem::swap(&mut self.min, &mut self.max);
325                            }
326                            let old_value = *self.value;
327                            let new_value = self.value.clamp(*self.min, *self.max);
328                            if (new_value - old_value).abs() > f32::EPSILON {
329                                ui.send_message(ScrollBarMessage::value(
330                                    self.handle(),
331                                    MessageDirection::ToWidget,
332                                    new_value,
333                                ));
334                            }
335
336                            let response = ScrollBarMessage::min_value(
337                                self.handle,
338                                MessageDirection::FromWidget,
339                                *self.min,
340                            );
341                            response.set_handled(message.handled());
342                            ui.send_message(response);
343                        }
344                    }
345                    ScrollBarMessage::MaxValue(max) => {
346                        if *self.max != max {
347                            self.max.set_value_and_mark_modified(max);
348                            if *self.max < *self.min {
349                                std::mem::swap(&mut self.min, &mut self.max);
350                            }
351                            let old_value = *self.value;
352                            let value = self.value.clamp(*self.min, *self.max);
353                            if (value - old_value).abs() > f32::EPSILON {
354                                ui.send_message(ScrollBarMessage::value(
355                                    self.handle(),
356                                    MessageDirection::ToWidget,
357                                    value,
358                                ));
359                            }
360
361                            let response = ScrollBarMessage::max_value(
362                                self.handle,
363                                MessageDirection::FromWidget,
364                                *self.max,
365                            );
366                            response.set_handled(message.handled());
367                            ui.send_message(response);
368                        }
369                    }
370                    ScrollBarMessage::SizeRatio(size_ratio) => {
371                        let field_size = ui.node(*self.indicator_canvas).actual_global_size();
372                        let indicator_size = ui.node(*self.indicator).actual_global_size();
373
374                        match *self.orientation {
375                            Orientation::Horizontal => {
376                                // minimum size of the indicator will be 15 irrespective of size ratio
377                                let new_size = (size_ratio * field_size.x).max(15.0);
378                                let old_size = indicator_size.x;
379
380                                if new_size != old_size {
381                                    ui.send_message(WidgetMessage::width(
382                                        *self.indicator,
383                                        MessageDirection::ToWidget,
384                                        new_size,
385                                    ));
386                                }
387                            }
388                            Orientation::Vertical => {
389                                // minimum size of the indicator will be 15 irrespective of size ratio
390                                let new_size = (size_ratio * field_size.y).max(15.0);
391                                let old_size = indicator_size.y;
392
393                                if new_size != old_size {
394                                    ui.send_message(WidgetMessage::height(
395                                        *self.indicator,
396                                        MessageDirection::ToWidget,
397                                        new_size,
398                                    ));
399                                }
400                            }
401                        }
402                    }
403                }
404            }
405        } else if let Some(msg) = message.data::<WidgetMessage>() {
406            if message.destination() == *self.indicator {
407                match msg {
408                    WidgetMessage::MouseDown { pos, .. } => {
409                        if self.indicator.is_some() {
410                            let indicator_pos = ui.nodes.borrow(*self.indicator).screen_position();
411                            self.is_dragging = true;
412                            self.offset = indicator_pos - *pos;
413                            ui.capture_mouse(*self.indicator);
414                            message.set_handled(true);
415                        }
416                    }
417                    WidgetMessage::MouseUp { .. } => {
418                        self.is_dragging = false;
419                        ui.release_mouse_capture();
420                        message.set_handled(true);
421                    }
422                    WidgetMessage::MouseMove { pos: mouse_pos, .. } => {
423                        if self.indicator.is_some() {
424                            let indicator_canvas = ui.node(*self.indicator_canvas);
425                            let indicator_size =
426                                ui.nodes.borrow(*self.indicator).actual_global_size();
427                            if self.is_dragging {
428                                let percent = match *self.orientation {
429                                    Orientation::Horizontal => {
430                                        let span = indicator_canvas.actual_global_size().x
431                                            - indicator_size.x;
432                                        let offset = mouse_pos.x
433                                            - indicator_canvas.screen_position().x
434                                            + self.offset.x;
435                                        if span > 0.0 {
436                                            (offset / span).clamp(0.0, 1.0)
437                                        } else {
438                                            0.0
439                                        }
440                                    }
441                                    Orientation::Vertical => {
442                                        let span = indicator_canvas.actual_global_size().y
443                                            - indicator_size.y;
444                                        let offset = mouse_pos.y
445                                            - indicator_canvas.screen_position().y
446                                            + self.offset.y;
447                                        if span > 0.0 {
448                                            (offset / span).clamp(0.0, 1.0)
449                                        } else {
450                                            0.0
451                                        }
452                                    }
453                                };
454                                ui.send_message(ScrollBarMessage::value(
455                                    self.handle(),
456                                    MessageDirection::ToWidget,
457                                    *self.min + percent * (*self.max - *self.min),
458                                ));
459                                message.set_handled(true);
460                            }
461                        }
462                    }
463                    _ => (),
464                }
465            }
466        }
467    }
468}
469
470/// Scroll bar widget is used to create [`ScrollBar`] widget instances and add them to the user interface.
471pub struct ScrollBarBuilder {
472    widget_builder: WidgetBuilder,
473    min: Option<f32>,
474    max: Option<f32>,
475    value: Option<f32>,
476    step: Option<f32>,
477    orientation: Option<Orientation>,
478    increase: Option<Handle<UiNode>>,
479    decrease: Option<Handle<UiNode>>,
480    indicator: Option<Handle<UiNode>>,
481    body: Option<Handle<UiNode>>,
482    show_value: bool,
483    value_precision: usize,
484    font: Option<FontResource>,
485    font_size: Option<StyledProperty<f32>>,
486}
487
488impl ScrollBarBuilder {
489    /// Creates new scroll bar builder instance.
490    pub fn new(widget_builder: WidgetBuilder) -> Self {
491        Self {
492            widget_builder,
493            min: None,
494            max: None,
495            value: None,
496            step: None,
497            orientation: None,
498            increase: None,
499            decrease: None,
500            indicator: None,
501            body: None,
502            show_value: false,
503            value_precision: 3,
504            font: None,
505            font_size: None,
506        }
507    }
508
509    /// Sets the desired min value.
510    pub fn with_min(mut self, min: f32) -> Self {
511        self.min = Some(min);
512        self
513    }
514
515    /// Sets the desired max value.
516    pub fn with_max(mut self, max: f32) -> Self {
517        self.max = Some(max);
518        self
519    }
520
521    /// Sets the desired value.
522    pub fn with_value(mut self, value: f32) -> Self {
523        self.value = Some(value);
524        self
525    }
526
527    /// Sets the desired orientation.
528    pub fn with_orientation(mut self, orientation: Orientation) -> Self {
529        self.orientation = Some(orientation);
530        self
531    }
532
533    /// Sets the desired step.
534    pub fn with_step(mut self, step: f32) -> Self {
535        self.step = Some(step);
536        self
537    }
538
539    /// Sets the new handle to a button, that is used to increase values of the scroll bar.
540    pub fn with_increase(mut self, increase: Handle<UiNode>) -> Self {
541        self.increase = Some(increase);
542        self
543    }
544
545    /// Sets the new handle to a button, that is used to decrease values of the scroll bar.
546    pub fn with_decrease(mut self, decrease: Handle<UiNode>) -> Self {
547        self.decrease = Some(decrease);
548        self
549    }
550
551    /// Sets the new handle to a widget, that is used as a thumb of the scroll bar.
552    pub fn with_indicator(mut self, indicator: Handle<UiNode>) -> Self {
553        self.indicator = Some(indicator);
554        self
555    }
556
557    /// Sets the new handle to a widget, that is used as a background of the scroll bar.
558    pub fn with_body(mut self, body: Handle<UiNode>) -> Self {
559        self.body = Some(body);
560        self
561    }
562
563    /// Show or hide the value of the scroll bar.
564    pub fn show_value(mut self, state: bool) -> Self {
565        self.show_value = state;
566        self
567    }
568
569    /// Sets the desired value precision of the scroll bar.
570    pub fn with_value_precision(mut self, precision: usize) -> Self {
571        self.value_precision = precision;
572        self
573    }
574
575    /// Sets the desired font.
576    pub fn with_font(mut self, font: FontResource) -> Self {
577        self.font = Some(font);
578        self
579    }
580
581    /// Sets the desired font size.
582    pub fn with_font_size(mut self, size: StyledProperty<f32>) -> Self {
583        self.font_size = Some(size);
584        self
585    }
586
587    /// Creates new scroll bar instance and adds it to the user interface.
588    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
589        let orientation = self.orientation.unwrap_or(Orientation::Horizontal);
590
591        let increase = self.increase.unwrap_or_else(|| {
592            ButtonBuilder::new(WidgetBuilder::new())
593                .with_content(match orientation {
594                    Orientation::Horizontal => make_arrow(ctx, ArrowDirection::Right, 8.0),
595                    Orientation::Vertical => make_arrow(ctx, ArrowDirection::Bottom, 8.0),
596                })
597                .with_repeat_clicks_on_hold(true)
598                .build(ctx)
599        });
600
601        match orientation {
602            Orientation::Vertical => {
603                ctx[increase].set_height(30.0).set_row(2).set_column(0);
604            }
605            Orientation::Horizontal => {
606                ctx[increase].set_width(30.0).set_row(0).set_column(2);
607            }
608        }
609
610        let decrease = self.decrease.unwrap_or_else(|| {
611            ButtonBuilder::new(WidgetBuilder::new())
612                .with_content(match orientation {
613                    Orientation::Horizontal => make_arrow(ctx, ArrowDirection::Left, 8.0),
614                    Orientation::Vertical => make_arrow(ctx, ArrowDirection::Top, 8.0),
615                })
616                .with_repeat_clicks_on_hold(true)
617                .build(ctx)
618        });
619
620        ctx[decrease].set_row(0).set_column(0);
621
622        match orientation {
623            Orientation::Vertical => ctx[decrease].set_height(30.0),
624            Orientation::Horizontal => ctx[decrease].set_width(30.0),
625        };
626
627        let indicator = self.indicator.unwrap_or_else(|| {
628            DecoratorBuilder::new(
629                BorderBuilder::new(
630                    WidgetBuilder::new().with_foreground(Brush::Solid(Color::TRANSPARENT).into()),
631                )
632                .with_corner_radius(8.0f32.into())
633                .with_pad_by_corner_radius(false)
634                .with_stroke_thickness(Thickness::uniform(1.0).into()),
635            )
636            .with_normal_brush(ctx.style.property(Style::BRUSH_LIGHT))
637            .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHTER))
638            .with_pressed_brush(ctx.style.property(Style::BRUSH_LIGHTEST))
639            .build(ctx)
640        });
641
642        match orientation {
643            Orientation::Vertical => {
644                ctx[indicator].set_min_size(Vector2::new(0.0, 15.0));
645            }
646            Orientation::Horizontal => {
647                ctx[indicator].set_min_size(Vector2::new(15.0, 0.0));
648            }
649        }
650
651        let min = self.min.unwrap_or(0.0);
652        let max = self.max.unwrap_or(100.0);
653        let value = self.value.unwrap_or(0.0).clamp(min, max);
654
655        let value_text = if self.show_value {
656            let value_text = TextBuilder::new(
657                WidgetBuilder::new()
658                    .with_visibility(self.show_value)
659                    .with_horizontal_alignment(HorizontalAlignment::Center)
660                    .with_vertical_alignment(VerticalAlignment::Center)
661                    .with_hit_test_visibility(false)
662                    .with_margin(Thickness::uniform(3.0))
663                    .on_column(match orientation {
664                        Orientation::Horizontal => 1,
665                        Orientation::Vertical => 0,
666                    })
667                    .on_row(match orientation {
668                        Orientation::Horizontal => 0,
669                        Orientation::Vertical => 1,
670                    }),
671            )
672            .with_font(self.font.unwrap_or_else(|| ctx.default_font()))
673            .with_font_size(
674                self.font_size
675                    .unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
676            )
677            .with_text(format!("{:.1$}", value, self.value_precision))
678            .build(ctx);
679
680            ctx.link(value_text, indicator);
681
682            value_text
683        } else {
684            Handle::NONE
685        };
686
687        let indicator_canvas = CanvasBuilder::new(
688            WidgetBuilder::new()
689                .on_column(match orientation {
690                    Orientation::Horizontal => 1,
691                    Orientation::Vertical => 0,
692                })
693                .on_row(match orientation {
694                    Orientation::Horizontal => 0,
695                    Orientation::Vertical => 1,
696                })
697                .with_child(indicator),
698        )
699        .build(ctx);
700
701        let grid = GridBuilder::new(
702            WidgetBuilder::new()
703                .with_child(decrease)
704                .with_child(indicator_canvas)
705                .with_child(increase),
706        )
707        .add_rows(match orientation {
708            Orientation::Horizontal => vec![Row::stretch()],
709            Orientation::Vertical => vec![Row::auto(), Row::stretch(), Row::auto()],
710        })
711        .add_columns(match orientation {
712            Orientation::Horizontal => vec![Column::auto(), Column::stretch(), Column::auto()],
713            Orientation::Vertical => vec![Column::stretch()],
714        })
715        .build(ctx);
716
717        let body = self.body.unwrap_or_else(|| {
718            BorderBuilder::new(
719                WidgetBuilder::new().with_background(ctx.style.property(Style::BRUSH_DARK)),
720            )
721            .with_stroke_thickness(Thickness::uniform(1.0).into())
722            .build(ctx)
723        });
724        ctx.link(grid, body);
725
726        let node = UiNode::new(ScrollBar {
727            widget: self.widget_builder.with_child(body).build(ctx),
728            min: min.into(),
729            max: max.into(),
730            value: value.into(),
731            step: self.step.unwrap_or(1.0).into(),
732            orientation: orientation.into(),
733            is_dragging: false,
734            offset: Vector2::default(),
735            increase: increase.into(),
736            decrease: decrease.into(),
737            indicator: indicator.into(),
738            indicator_canvas: indicator_canvas.into(),
739            value_text: value_text.into(),
740            value_precision: self.value_precision.into(),
741        });
742        ctx.add_node(node)
743    }
744}
745
746#[cfg(test)]
747mod test {
748    use crate::scroll_bar::ScrollBarBuilder;
749    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
750
751    #[test]
752    fn test_deletion() {
753        test_widget_deletion(|ctx| ScrollBarBuilder::new(WidgetBuilder::new()).build(ctx));
754    }
755}