Skip to main content

gpui_base/
slider.rs

1use crate::TestSupportExt as _;
2use std::ops::Range;
3
4use gpui::{
5    AccessibleAction, Along, AnyElement, App, AppContext as _, Axis, Bounds, Context, Div,
6    DragMoveEvent, Empty, Entity, EntityId, EventEmitter, FocusHandle, HitboxBehavior,
7    InteractiveElement, IntoElement, MouseButton, MouseDownEvent, Orientation, ParentElement,
8    Pixels, Point, Render, RenderOnce, Role, SharedString, StatefulInteractiveElement,
9    StyleRefinement, Styled, TouchDragEvent, TouchPhase, Window, div, prelude::FluentBuilder as _,
10    px,
11};
12
13use crate::{a11y_disabled::A11yDisabledExt as _, element_ext::ElementExt, geometry::AxisExt};
14
15/// Events emitted by the [`SliderState`].
16pub enum SliderEvent {
17    /// Emitted continuously while the slider value is being changed by the user.
18    Change(SliderValue),
19    /// Emitted once when the user releases the slider after a drag or click.
20    Release(SliderValue),
21}
22
23/// The value of the slider, can be a single value or a range of values.
24///
25/// - Can from a f32 value, which will be treated as a single value.
26/// - Or from a (f32, f32) tuple, which will be treated as a range of values.
27///
28/// The default value is `SliderValue::Single(0.0)`.
29#[derive(Clone, Copy, Debug, PartialEq)]
30pub enum SliderValue {
31    Single(f32),
32    Range(f32, f32),
33}
34
35impl std::fmt::Display for SliderValue {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            SliderValue::Single(value) => write!(f, "{}", value),
39            SliderValue::Range(start, end) => write!(f, "{}..{}", start, end),
40        }
41    }
42}
43
44impl From<f32> for SliderValue {
45    fn from(value: f32) -> Self {
46        SliderValue::Single(value)
47    }
48}
49
50impl From<(f32, f32)> for SliderValue {
51    fn from(value: (f32, f32)) -> Self {
52        SliderValue::Range(value.0, value.1)
53    }
54}
55
56impl From<Range<f32>> for SliderValue {
57    fn from(value: Range<f32>) -> Self {
58        SliderValue::Range(value.start, value.end)
59    }
60}
61
62impl Default for SliderValue {
63    fn default() -> Self {
64        SliderValue::Single(0.)
65    }
66}
67
68impl SliderValue {
69    /// Clamp the value to the given range.
70    pub fn clamp(self, min: f32, max: f32) -> Self {
71        match self {
72            SliderValue::Single(value) => SliderValue::Single(value.clamp(min, max)),
73            SliderValue::Range(start, end) => {
74                SliderValue::Range(start.clamp(min, max), end.clamp(min, max))
75            }
76        }
77    }
78
79    /// Check if the value is a single value.
80    #[inline]
81    pub fn is_single(&self) -> bool {
82        matches!(self, SliderValue::Single(_))
83    }
84
85    /// Check if the value is a range of values.
86    #[inline]
87    pub fn is_range(&self) -> bool {
88        matches!(self, SliderValue::Range(_, _))
89    }
90
91    /// Get the start value.
92    pub fn start(&self) -> f32 {
93        match self {
94            SliderValue::Single(value) => *value,
95            SliderValue::Range(start, _) => *start,
96        }
97    }
98
99    /// Get the end value.
100    pub fn end(&self) -> f32 {
101        match self {
102            SliderValue::Single(value) => *value,
103            SliderValue::Range(_, end) => *end,
104        }
105    }
106
107    fn set_start(&mut self, value: f32) {
108        if let SliderValue::Range(_, end) = self {
109            *self = SliderValue::Range(value.min(*end), *end);
110        } else {
111            *self = SliderValue::Single(value);
112        }
113    }
114
115    fn set_end(&mut self, value: f32) {
116        if let SliderValue::Range(start, _) = self {
117            *self = SliderValue::Range(*start, value.max(*start));
118        } else {
119            *self = SliderValue::Single(value);
120        }
121    }
122}
123
124/// The scale mode of the slider.
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
126pub enum SliderScale {
127    /// Linear scale where values change uniformly across the slider range.
128    /// This is the default mode.
129    #[default]
130    Linear,
131    /// Logarithmic scale where the distance between values increases exponentially.
132    ///
133    /// This is useful for parameters that have a large range of values where smaller
134    /// changes are more significant at lower values. Common examples include:
135    ///
136    /// - Volume controls (human hearing perception is logarithmic)
137    /// - Frequency controls (musical notes follow a logarithmic scale)
138    /// - Zoom levels
139    /// - Any parameter where you want finer control at lower values
140    ///
141    /// # For example
142    ///
143    /// ```
144    /// use gpui_base::slider::{SliderScale, SliderState};
145    ///
146    /// let slider = SliderState::new()
147    ///     .min(1.0)    // Must be > 0 for logarithmic scale
148    ///     .max(1000.0)
149    ///     .scale(SliderScale::Logarithmic);
150    /// ```
151    ///
152    /// - Moving the slider 1/3 of the way will yield ~10
153    /// - Moving it 2/3 of the way will yield ~100
154    /// - The full range covers 3 orders of magnitude evenly
155    Logarithmic,
156}
157
158impl SliderScale {
159    #[inline]
160    pub fn is_linear(&self) -> bool {
161        matches!(self, SliderScale::Linear)
162    }
163
164    #[inline]
165    pub fn is_logarithmic(&self) -> bool {
166        matches!(self, SliderScale::Logarithmic)
167    }
168}
169
170/// State of the [`Slider`].
171pub struct SliderState {
172    min: f32,
173    max: f32,
174    step: f32,
175    value: SliderValue,
176    /// When is single value mode, only `end` is used, the start is always 0.0.
177    percentage: Range<f32>,
178    /// The bounds of the slider after rendered.
179    bounds: Bounds<Pixels>,
180    scale: SliderScale,
181    /// Tracks whether the user is currently interacting with the slider so we
182    /// only emit [`SliderEvent::Release`] after a real press/drag.
183    dragging: bool,
184    /// The touch drag this slider claimed, while one is live.
185    touch_drag: Option<TouchDrag>,
186}
187
188/// A touch drag claimed by a slider.
189#[derive(Clone, Copy)]
190struct TouchDrag {
191    /// Identifies the gesture: every event of one drag carries the same start.
192    start_position: Point<Pixels>,
193    /// Whether it moves the range start thumb.
194    is_start: bool,
195}
196
197impl SliderState {
198    /// Create a new [`SliderState`].
199    pub fn new() -> Self {
200        Self {
201            min: 0.0,
202            max: 100.0,
203            step: 1.0,
204            value: SliderValue::default(),
205            percentage: (0.0..0.0),
206            bounds: Bounds::default(),
207            scale: SliderScale::default(),
208            dragging: false,
209            touch_drag: None,
210        }
211    }
212
213    /// Set the minimum value of the slider, default: 0.0
214    pub fn min(mut self, min: f32) -> Self {
215        if self.scale.is_logarithmic() {
216            assert!(
217                min > 0.0,
218                "`min` must be greater than 0 for SliderScale::Logarithmic"
219            );
220            assert!(
221                min < self.max,
222                "`min` must be less than `max` for Logarithmic scale"
223            );
224        }
225        self.min = min;
226        self.update_thumb_pos();
227        self
228    }
229
230    /// Set the maximum value of the slider, default: 100.0
231    pub fn max(mut self, max: f32) -> Self {
232        if self.scale.is_logarithmic() {
233            assert!(
234                max > self.min,
235                "`max` must be greater than `min` for Logarithmic scale"
236            );
237        }
238        self.max = max;
239        self.update_thumb_pos();
240        self
241    }
242
243    /// Set the step value of the slider, default: 1.0
244    pub fn step(mut self, step: f32) -> Self {
245        self.step = step;
246        self
247    }
248
249    /// Set the scale of the slider, default: [`SliderScale::Linear`].
250    pub fn scale(mut self, scale: SliderScale) -> Self {
251        if scale.is_logarithmic() {
252            assert!(
253                self.min > 0.0,
254                "`min` must be greater than 0 for Logarithmic scale"
255            );
256            assert!(
257                self.max > self.min,
258                "`max` must be greater than `min` for Logarithmic scale"
259            );
260        }
261        self.scale = scale;
262        self.update_thumb_pos();
263        self
264    }
265
266    /// Set the default value of the slider, default: 0.0
267    pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
268        self.value = value.into();
269        self.update_thumb_pos();
270        self
271    }
272
273    /// Set the value of the slider.
274    pub fn set_value(
275        &mut self,
276        value: impl Into<SliderValue>,
277        _: &mut Window,
278        cx: &mut Context<Self>,
279    ) {
280        self.value = value.into();
281        self.update_thumb_pos();
282        cx.notify();
283    }
284
285    /// Get the value of the slider.
286    pub fn value(&self) -> SliderValue {
287        self.value
288    }
289
290    /// Get the minimum value.
291    pub fn min_value(&self) -> f32 {
292        self.min
293    }
294
295    /// Get the maximum value.
296    pub fn max_value(&self) -> f32 {
297        self.max
298    }
299
300    /// Get the step value.
301    pub fn step_value(&self) -> f32 {
302        self.step
303    }
304
305    /// The amount an assistive technology's Increment and Decrement move a thumb, and the
306    /// step reported to it: the step, or a hundredth of the range for a step that is not a
307    /// positive finite number (a continuous slider).
308    pub fn accessibility_step(&self) -> f32 {
309        if is_step(self.step) {
310            self.step
311        } else {
312            (self.max - self.min) / 100.
313        }
314    }
315
316    /// Converts a value between 0.0 and 1.0 to a value between the minimum and maximum value,
317    /// depending on the chosen scale.
318    fn percentage_to_value(&self, percentage: f32) -> f32 {
319        match self.scale {
320            SliderScale::Linear => self.min + (self.max - self.min) * percentage,
321            SliderScale::Logarithmic => {
322                // when percentage is 0, this simplifies to (max/min)^0 * min = 1 * min = min
323                // when percentage is 1, this simplifies to (max/min)^1 * min = (max*min)/min = max
324                // we clamp just to make sure we don't have issue with floating point precision
325                let base = self.max / self.min;
326                (base.powf(percentage) * self.min).clamp(self.min, self.max)
327            }
328        }
329    }
330
331    /// Converts a value between the minimum and maximum value to a value between 0.0 and 1.0,
332    /// depending on the chosen scale.
333    fn value_to_percentage(&self, value: f32) -> f32 {
334        match self.scale {
335            SliderScale::Linear => {
336                let range = self.max - self.min;
337                if range <= 0.0 {
338                    0.0
339                } else {
340                    (value - self.min) / range
341                }
342            }
343            SliderScale::Logarithmic => {
344                let base = self.max / self.min;
345                (value / self.min).log(base).clamp(0.0, 1.0)
346            }
347        }
348    }
349
350    fn update_thumb_pos(&mut self) {
351        match self.value {
352            SliderValue::Single(value) => {
353                let percentage = self.value_to_percentage(value.min(self.max).max(self.min));
354                self.percentage = 0.0..percentage;
355            }
356            SliderValue::Range(start, end) => {
357                // Not `clamp`: while a builder is between its `min` and `max`, `min`
358                // can stand above `max`.
359                let clamped_start = start.min(self.max).max(self.min);
360                let clamped_end = end.min(self.max).max(self.min);
361                self.percentage =
362                    self.value_to_percentage(clamped_start)..self.value_to_percentage(clamped_end);
363            }
364        }
365    }
366
367    /// Update value by mouse position
368    #[doc(hidden)]
369    pub fn update_value_by_position(
370        &mut self,
371        axis: Axis,
372        position: Point<Pixels>,
373        is_start: bool,
374        _: &mut Window,
375        cx: &mut Context<Self>,
376    ) {
377        self.dragging = true;
378        let bounds = self.bounds;
379        let step = self.step;
380
381        let inner_pos = if axis.is_horizontal() {
382            position.x - bounds.left()
383        } else {
384            bounds.bottom() - position.y
385        };
386        let total_size = bounds.size.along(axis);
387        let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
388
389        let percentage = if is_start {
390            percentage.clamp(0.0, self.percentage.end)
391        } else {
392            percentage.clamp(self.percentage.start, 1.0)
393        };
394
395        let value = self.percentage_to_value(percentage);
396        let value = snap_to_step(value, self.min, self.max, step);
397
398        if is_start {
399            self.value.set_start(value);
400        } else {
401            self.value.set_end(value);
402        }
403        // The thumb stands at the value, on its step, not at the pointer.
404        self.update_thumb_pos();
405        cx.emit(SliderEvent::Change(self.value));
406        cx.notify();
407    }
408
409    /// Moves the end thumb or, with `start`, a range's start thumb by `delta`, as an
410    /// assistive technology's Increment and Decrement do, and emits
411    /// [`SliderEvent::Change`] if it moved. The thumb lands on the step grid counted from
412    /// `min` and stops at the other thumb and at the slider's ends.
413    #[doc(hidden)]
414    pub fn step_thumb(
415        &mut self,
416        start: bool,
417        delta: f32,
418        window: &mut Window,
419        cx: &mut Context<Self>,
420    ) {
421        // A NaN never equals itself, so it would read as a change below.
422        if !steppable(self.value, self.min, self.max) {
423            return;
424        }
425        let value = step_thumb(self.value, start, delta, self.min, self.max, self.step);
426        if value != self.value {
427            self.set_value(value, window, cx);
428            cx.emit(SliderEvent::Change(value));
429        }
430    }
431
432    /// Whether `position` is closer to the range start thumb than to the end thumb.
433    fn is_nearer_start(&self, axis: Axis, position: Point<Pixels>) -> bool {
434        let size = self.bounds.size.along(axis);
435        let along = if axis.is_horizontal() {
436            position.x - self.bounds.left()
437        } else {
438            self.bounds.bottom() - position.y
439        };
440        let center =
441            ((self.percentage.end - self.percentage.start) / 2. + self.percentage.start) * size;
442        along < center
443    }
444
445    /// Emit [`SliderEvent::Release`] if the user was actively interacting
446    /// with the slider. Called on mouse-up both inside and outside the slider.
447    #[doc(hidden)]
448    pub fn handle_release(&mut self, cx: &mut Context<Self>) {
449        if !self.dragging {
450            return;
451        }
452        self.dragging = false;
453        cx.emit(SliderEvent::Release(self.value));
454    }
455}
456
457/// Whether `step` divides the range into steps: a positive finite number.
458fn is_step(step: f32) -> bool {
459    step > 0. && step.is_finite()
460}
461
462/// `value` on the nearest step counted from `min`, kept inside the range. A step that is
463/// not a positive finite number leaves the value continuous.
464fn snap_to_step(value: f32, min: f32, max: f32, step: f32) -> f32 {
465    if !is_step(step) {
466        return value;
467    }
468    (min + ((value - min) / step).round() * step)
469        .min(max)
470        .max(min)
471}
472
473/// Whether `value` can be stepped inside `min..=max`: not for a NaN value or bound, a
474/// `min` above `max`, or a range whose start is past its end.
475fn steppable(value: SliderValue, min: f32, max: f32) -> bool {
476    let (start, end) = (value.start(), value.end());
477    min <= max && !start.is_nan() && !end.is_nan() && (!value.is_range() || start <= end)
478}
479
480/// `value` with its end, or with `start` a range's start, moved by `delta` and snapped to
481/// `step` counted from `min`, inside `min..=max` and without passing the other thumb. A
482/// value that is not [`steppable`] comes back unchanged.
483fn step_thumb(
484    value: SliderValue,
485    start: bool,
486    delta: f32,
487    min: f32,
488    max: f32,
489    step: f32,
490) -> SliderValue {
491    if !steppable(value, min, max) {
492        return value;
493    }
494    let mut value = value;
495    let moves_start = start && value.is_range();
496    let current = if moves_start {
497        value.start()
498    } else {
499        value.end()
500    };
501    let next = snap_to_step(current + delta, min, max, step)
502        .min(max)
503        .max(min);
504    if moves_start {
505        value.set_start(next);
506    } else {
507        value.set_end(next);
508    }
509    value
510}
511
512#[derive(Clone)]
513struct DragThumb((EntityId, bool));
514
515impl Render for DragThumb {
516    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
517        Empty
518    }
519}
520
521#[derive(Clone)]
522struct DragSlider(EntityId);
523
524impl Render for DragSlider {
525    fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
526        Empty
527    }
528}
529
530/// An unstyled slider behavior root.
531///
532/// Applications provide the track, range, and thumb presentation as children.
533#[derive(IntoElement)]
534pub struct Slider {
535    state: Entity<SliderState>,
536    axis: Axis,
537    disabled: bool,
538    accessibility_label: Option<SharedString>,
539    focus: Option<FocusHandle>,
540    base: Div,
541    children: Vec<AnyElement>,
542}
543
544impl Slider {
545    pub fn new(state: &Entity<SliderState>) -> Self {
546        Self {
547            state: state.clone(),
548            axis: Axis::Horizontal,
549            disabled: false,
550            accessibility_label: None,
551            focus: None,
552            base: div(),
553            children: Vec::new(),
554        }
555    }
556
557    pub fn horizontal(mut self) -> Self {
558        self.axis = Axis::Horizontal;
559        self
560    }
561
562    pub fn vertical(mut self) -> Self {
563        self.axis = Axis::Vertical;
564        self
565    }
566
567    pub fn axis(mut self, axis: Axis) -> Self {
568        self.axis = axis;
569        self
570    }
571
572    pub fn disabled(mut self, disabled: bool) -> Self {
573        self.disabled = disabled;
574        self
575    }
576
577    /// Sets the label exposed to accessibility clients.
578    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
579        self.accessibility_label = Some(label.into());
580        self
581    }
582
583    /// Makes the slider's accessibility node itself focusable with `focus`,
584    /// so keyboard focus and assistive technology land on the slider.
585    pub fn track_focus(mut self, focus: &FocusHandle) -> Self {
586        self.focus = Some(focus.clone());
587        self
588    }
589}
590
591impl ParentElement for Slider {
592    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
593        self.children.extend(elements);
594    }
595}
596
597impl Styled for Slider {
598    fn style(&mut self) -> &mut StyleRefinement {
599        self.base.style()
600    }
601}
602
603impl RenderOnce for Slider {
604    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
605        let axis = self.axis;
606        let entity_id = self.state.entity_id();
607        let state = self.state.read(cx);
608        let slider_state = self.state.clone();
609
610        self.base
611            .id(("slider", entity_id))
612            .test_support()
613            .when_some(self.focus, |this, focus| this.track_focus(&focus))
614            .role(Role::Slider)
615            .when_some(self.accessibility_label, |this, label| {
616                this.aria_label(label)
617            })
618            .aria_numeric_value(state.value().end() as f64)
619            .aria_min_numeric_value(state.min_value() as f64)
620            .aria_max_numeric_value(state.max_value() as f64)
621            .aria_numeric_value_step(state.accessibility_step() as f64)
622            .aria_orientation(if axis.is_vertical() {
623                Orientation::Vertical
624            } else {
625                Orientation::Horizontal
626            })
627            // A disabled slider takes no value, from the pointer or from
628            // assistive technology.
629            .when(!self.disabled, |this| {
630                this.on_a11y_action(AccessibleAction::Increment, {
631                    let state = slider_state.clone();
632                    move |_, window, cx| {
633                        state.update(cx, |state, cx| {
634                            state.step_thumb(false, state.accessibility_step(), window, cx);
635                        });
636                    }
637                })
638                .on_a11y_action(AccessibleAction::Decrement, {
639                    let state = slider_state.clone();
640                    move |_, window, cx| {
641                        state.update(cx, |state, cx| {
642                            state.step_thumb(false, -state.accessibility_step(), window, cx);
643                        });
644                    }
645                })
646                .on_mouse_up(
647                    MouseButton::Left,
648                    window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
649                )
650                .on_mouse_up_out(
651                    MouseButton::Left,
652                    window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
653                )
654            })
655            .children(self.children)
656            .a11y_disabled(self.disabled)
657    }
658}
659
660/// An unstyled track that records the geometry used to map pointer positions.
661#[derive(IntoElement)]
662pub struct SliderTrack {
663    state: Entity<SliderState>,
664    axis: Axis,
665    disabled: bool,
666    base: Div,
667    children: Vec<AnyElement>,
668}
669
670impl SliderTrack {
671    pub fn new(state: &Entity<SliderState>) -> Self {
672        Self {
673            state: state.clone(),
674            axis: Axis::Horizontal,
675            disabled: false,
676            base: div(),
677            children: Vec::new(),
678        }
679    }
680
681    pub fn axis(mut self, axis: Axis) -> Self {
682        self.axis = axis;
683        self
684    }
685    pub fn disabled(mut self, disabled: bool) -> Self {
686        self.disabled = disabled;
687        self
688    }
689}
690
691impl ParentElement for SliderTrack {
692    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
693        self.children.extend(elements);
694    }
695}
696
697impl Styled for SliderTrack {
698    fn style(&mut self) -> &mut StyleRefinement {
699        self.base.style()
700    }
701}
702
703impl InteractiveElement for SliderTrack {
704    fn interactivity(&mut self) -> &mut gpui::Interactivity {
705        self.base.interactivity()
706    }
707}
708
709impl RenderOnce for SliderTrack {
710    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
711        let axis = self.axis;
712        let entity_id = self.state.entity_id();
713        let state = self.state.read(cx);
714        let is_range = state.value().is_range();
715        let percentage = state.percentage();
716        // Touch: GPUI routes a finger drag to `TouchDragEvent` (claimed with
717        // `prevent_default`) or to scrolling; `on_drag` below is mouse-only. Claim
718        // drags that start on the track, as the scrollbar thumb does.
719        let touch_layer = (!self.disabled).then(|| {
720            let slider_state = self.state.clone();
721            gpui::canvas(
722                |bounds, window, _| window.insert_hitbox(bounds, HitboxBehavior::Normal),
723                move |_, hitbox, window, _| {
724                    let slider_state = slider_state.clone();
725                    window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
726                        if !phase.bubble() {
727                            return;
728                        }
729                        slider_state.update(cx, |state, cx| {
730                            if event.phase == TouchPhase::Started {
731                                // A drag whose end never arrived (the slider was
732                                // disabled or not painted) ends when another begins.
733                                if state.touch_drag.take().is_some() {
734                                    state.handle_release(cx);
735                                }
736                                // The hitbox, not the bounds, so a layer covering
737                                // the track keeps its own drags.
738                                if window.default_prevented() || !hitbox.is_hovered(window) {
739                                    return;
740                                }
741                                window.prevent_default();
742                                let is_start =
743                                    is_range && state.is_nearer_start(axis, event.start_position);
744                                state.touch_drag = Some(TouchDrag {
745                                    start_position: event.start_position,
746                                    is_start,
747                                });
748                            }
749                            let Some(drag) = state.touch_drag else {
750                                return;
751                            };
752                            if drag.start_position != event.start_position {
753                                return;
754                            }
755                            cx.stop_propagation();
756                            match event.phase {
757                                TouchPhase::Started | TouchPhase::Moved => {
758                                    state.update_value_by_position(
759                                        axis,
760                                        event.position,
761                                        drag.is_start,
762                                        window,
763                                        cx,
764                                    );
765                                }
766                                TouchPhase::Ended | TouchPhase::Cancelled => {
767                                    state.touch_drag = None;
768                                    state.handle_release(cx);
769                                }
770                            }
771                        });
772                    });
773                },
774            )
775            .absolute()
776            .size_full()
777        });
778        self.base
779            .id("slider-bar-container")
780            .test_support()
781            .children(self.children)
782            .children(touch_layer)
783            .when(!self.disabled, |this| {
784                this.on_mouse_down(
785                    MouseButton::Left,
786                    window.listener_for(
787                        &self.state,
788                        move |state, event: &MouseDownEvent, window, cx| {
789                            let is_start = if is_range {
790                                let size = state.bounds().size.along(axis);
791                                let position = if axis.is_horizontal() {
792                                    event.position.x - state.bounds().left()
793                                } else {
794                                    state.bounds().bottom() - event.position.y
795                                };
796                                let center = ((percentage.end - percentage.start) / 2.
797                                    + percentage.start)
798                                    * size;
799                                position < center
800                            } else {
801                                false
802                            };
803                            state.update_value_by_position(
804                                axis,
805                                event.position,
806                                is_start,
807                                window,
808                                cx,
809                            );
810                        },
811                    ),
812                )
813                .when(!is_range, |this| {
814                    this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
815                        cx.stop_propagation();
816                        cx.new(|_| drag.clone())
817                    })
818                    .on_drag_move(window.listener_for(
819                        &self.state,
820                        move |state, event: &DragMoveEvent<DragSlider>, window, cx| {
821                            let DragSlider(id) = event.drag(cx);
822                            if *id == entity_id {
823                                state.update_value_by_position(
824                                    axis,
825                                    event.event.position,
826                                    false,
827                                    window,
828                                    cx,
829                                );
830                            }
831                        },
832                    ))
833                })
834            })
835    }
836}
837
838/// An unstyled slider indicator that records the value-mapping bounds.
839#[derive(IntoElement)]
840pub struct SliderIndicator {
841    state: Entity<SliderState>,
842    base: Div,
843    children: Vec<AnyElement>,
844}
845
846impl SliderIndicator {
847    pub fn new(state: &Entity<SliderState>) -> Self {
848        Self {
849            state: state.clone(),
850            base: div(),
851            children: Vec::new(),
852        }
853    }
854}
855
856impl ParentElement for SliderIndicator {
857    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
858        self.children.extend(elements);
859    }
860}
861
862impl Styled for SliderIndicator {
863    fn style(&mut self) -> &mut StyleRefinement {
864        self.base.style()
865    }
866}
867
868impl InteractiveElement for SliderIndicator {
869    fn interactivity(&mut self) -> &mut gpui::Interactivity {
870        self.base.interactivity()
871    }
872}
873
874impl StatefulInteractiveElement for SliderIndicator {}
875
876impl RenderOnce for SliderIndicator {
877    fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
878        self.base
879            .id("slider-bar")
880            .children(self.children)
881            .on_prepaint({
882                let state = self.state;
883                // Only when they moved: with retained views, an update while
884                // the window draws renders every view that reads the state
885                // again.
886                move |bounds, _, cx| {
887                    if state.read(cx).bounds != bounds {
888                        state.update(cx, |state, _| state.set_bounds(bounds));
889                    }
890                }
891            })
892    }
893}
894
895/// An unstyled draggable slider thumb.
896#[derive(IntoElement)]
897pub struct SliderThumb {
898    state: Entity<SliderState>,
899    axis: Axis,
900    start: bool,
901    disabled: bool,
902    base: Div,
903    children: Vec<AnyElement>,
904}
905
906impl SliderThumb {
907    pub fn new(state: &Entity<SliderState>) -> Self {
908        Self {
909            state: state.clone(),
910            axis: Axis::Horizontal,
911            start: false,
912            disabled: false,
913            base: div(),
914            children: Vec::new(),
915        }
916    }
917
918    pub fn axis(mut self, axis: Axis) -> Self {
919        self.axis = axis;
920        self
921    }
922    pub fn start(mut self, start: bool) -> Self {
923        self.start = start;
924        self
925    }
926    pub fn disabled(mut self, disabled: bool) -> Self {
927        self.disabled = disabled;
928        self
929    }
930}
931
932impl ParentElement for SliderThumb {
933    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
934        self.children.extend(elements);
935    }
936}
937
938impl Styled for SliderThumb {
939    fn style(&mut self) -> &mut StyleRefinement {
940        self.base.style()
941    }
942}
943
944impl InteractiveElement for SliderThumb {
945    fn interactivity(&mut self) -> &mut gpui::Interactivity {
946        self.base.interactivity()
947    }
948}
949
950impl StatefulInteractiveElement for SliderThumb {}
951
952impl RenderOnce for SliderThumb {
953    fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
954        let entity_id = self.state.entity_id();
955        let axis = self.axis;
956        let start = self.start;
957        self.base
958            .id(("slider-thumb", start as u32))
959            .test_support()
960            .children(self.children)
961            .when(!self.disabled, |this| {
962                this.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
963                    .on_drag(DragThumb((entity_id, start)), |drag, _, _, cx| {
964                        cx.stop_propagation();
965                        cx.new(|_| drag.clone())
966                    })
967                    .on_drag_move(window.listener_for(
968                        &self.state,
969                        move |state, event: &DragMoveEvent<DragThumb>, window, cx| {
970                            let DragThumb((id, start)) = event.drag(cx);
971                            if *id == entity_id {
972                                state.update_value_by_position(
973                                    axis,
974                                    event.event.position,
975                                    *start,
976                                    window,
977                                    cx,
978                                );
979                            }
980                        },
981                    ))
982            })
983            // A caller can give a thumb a role of its own, as for a range's thumbs.
984            .a11y_disabled(self.disabled)
985    }
986}
987
988impl EventEmitter<SliderEvent> for SliderState {}
989
990impl SliderState {
991    #[doc(hidden)]
992    pub fn percentage(&self) -> Range<f32> {
993        self.percentage.clone()
994    }
995
996    #[doc(hidden)]
997    pub fn bounds(&self) -> Bounds<Pixels> {
998        self.bounds
999    }
1000
1001    #[doc(hidden)]
1002    pub fn set_bounds(&mut self, bounds: Bounds<Pixels>) {
1003        self.bounds = bounds;
1004    }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use super::*;
1010
1011    #[test]
1012    fn legacy_value_conversions_and_clamping_are_preserved() {
1013        assert_eq!(SliderValue::from(5.), SliderValue::Single(5.));
1014        assert_eq!(SliderValue::from((2., 8.)), SliderValue::Range(2., 8.));
1015        assert_eq!(SliderValue::from(2.0..8.0), SliderValue::Range(2., 8.));
1016        assert_eq!(
1017            SliderValue::Range(-1., 12.).clamp(0., 10.),
1018            SliderValue::Range(0., 10.)
1019        );
1020    }
1021
1022    #[test]
1023    fn legacy_linear_state_keeps_percentage_and_range_ordering() {
1024        let state = SliderState::new()
1025            .min(0.)
1026            .max(200.)
1027            .default_value((50., 150.));
1028        assert_eq!(state.value(), SliderValue::Range(50., 150.));
1029        assert_eq!(state.percentage(), 0.25..0.75);
1030    }
1031
1032    #[test]
1033    fn legacy_logarithmic_state_keeps_mapping() {
1034        let state = SliderState::new()
1035            .min(1.)
1036            .max(1000.)
1037            .scale(SliderScale::Logarithmic)
1038            .default_value(10.);
1039        let percentage = state.percentage().end;
1040        assert!((percentage - (1. / 3.)).abs() < 0.0001);
1041    }
1042
1043    #[gpui::test]
1044    fn a_disabled_thumb_marks_its_node_disabled(cx: &mut gpui::TestAppContext) {
1045        struct Thumb {
1046            state: Entity<SliderState>,
1047            disabled: bool,
1048        }
1049
1050        impl Render for Thumb {
1051            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1052                SliderThumb::new(&self.state)
1053                    .role(Role::Slider)
1054                    .disabled(self.disabled)
1055            }
1056        }
1057
1058        for disabled in [false, true] {
1059            let (_, cx) = cx.add_window_view(|_, cx| Thumb {
1060                state: cx.new(|_| SliderState::new()),
1061                disabled,
1062            });
1063            cx.update(|window, cx| {
1064                window.draw(cx).clear(cx);
1065                let thumb = crate::test_support::snapshots(window)
1066                    .into_iter()
1067                    .find(|node| node.role() == Some(Role::Slider))
1068                    .expect("a thumb node");
1069                assert_eq!(thumb.disabled(), disabled.then_some(true));
1070            });
1071        }
1072    }
1073
1074    #[gpui::test]
1075    fn a_slider_takes_focus_on_its_own_node(cx: &mut gpui::TestAppContext) {
1076        struct Focusable {
1077            state: Entity<SliderState>,
1078            focus: FocusHandle,
1079        }
1080
1081        impl Render for Focusable {
1082            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1083                Slider::new(&self.state).track_focus(&self.focus)
1084            }
1085        }
1086
1087        let (view, cx) = cx.add_window_view(|_, cx| Focusable {
1088            state: cx.new(|_| SliderState::new()),
1089            focus: cx.focus_handle(),
1090        });
1091        cx.update(|window, cx| {
1092            let focus = view.read(cx).focus.clone();
1093            focus.focus(window, cx);
1094            window.draw(cx).clear(cx);
1095            let slider = crate::test_support::snapshots(window)
1096                .into_iter()
1097                .find(|node| node.role() == Some(Role::Slider))
1098                .expect("a slider node");
1099            assert_eq!(slider.focused(), Some(true));
1100        });
1101    }
1102
1103    #[test]
1104    fn min_can_be_set_above_the_default_max_before_max() {
1105        let state = SliderState::new().min(150.).max(200.).default_value(175.);
1106        assert_eq!(state.value(), SliderValue::Single(175.));
1107        assert_eq!(state.percentage(), 0.0..0.5);
1108
1109        let state = SliderState::new()
1110            .default_value((160., 180.))
1111            .min(150.)
1112            .max(200.);
1113        assert_eq!(state.percentage(), 0.2..0.6);
1114    }
1115
1116    #[test]
1117    fn dragging_snaps_from_min() {
1118        // Steps count from `min`: 3 + 5k, not multiples of 5.
1119        assert_eq!(snap_to_step(9.4, 3., 100., 5.), 8.);
1120        assert_eq!(snap_to_step(11., 3., 100., 5.), 13.);
1121        // A step past the end stays inside the range.
1122        assert_eq!(snap_to_step(99., 3., 100., 5.), 98.);
1123        assert_eq!(snap_to_step(100., 0., 99., 10.), 99.);
1124        // A step that is not a positive finite number leaves the value continuous.
1125        assert_eq!(snap_to_step(42.5, 0., 100., 0.), 42.5);
1126        assert_eq!(snap_to_step(42.5, 0., 100., -5.), 42.5);
1127        assert_eq!(snap_to_step(42.5, 0., 100., f32::NAN), 42.5);
1128        assert_eq!(snap_to_step(42.5, 0., 100., f32::INFINITY), 42.5);
1129    }
1130
1131    #[test]
1132    fn accessibility_steps_move_only_the_stepped_thumb() {
1133        assert_eq!(
1134            step_thumb(SliderValue::Range(20., 60.), false, 1., 0., 100., 1.),
1135            SliderValue::Range(20., 61.)
1136        );
1137        assert_eq!(
1138            step_thumb(SliderValue::Range(20., 20.), false, -1., 0., 100., 1.),
1139            SliderValue::Range(20., 20.),
1140            "the end never passes the start"
1141        );
1142        assert_eq!(
1143            step_thumb(SliderValue::Single(99.5), false, 1., 0., 100., 1.),
1144            SliderValue::Single(100.)
1145        );
1146        assert_eq!(
1147            step_thumb(SliderValue::Range(20., 60.), true, -1., 0., 100., 1.),
1148            SliderValue::Range(19., 60.)
1149        );
1150        assert_eq!(
1151            step_thumb(SliderValue::Range(60., 60.), true, 1., 0., 100., 1.),
1152            SliderValue::Range(60., 60.),
1153            "the start never passes the end"
1154        );
1155    }
1156
1157    #[test]
1158    fn accessibility_steps_land_on_the_step_grid() {
1159        assert_eq!(
1160            step_thumb(SliderValue::Single(33.), false, 10., 0., 100., 10.),
1161            SliderValue::Single(40.)
1162        );
1163        assert_eq!(
1164            step_thumb(SliderValue::Single(33.), false, -10., 0., 100., 10.),
1165            SliderValue::Single(20.)
1166        );
1167        assert_eq!(
1168            step_thumb(SliderValue::Single(33.), false, 0.5, 0., 100., 0.),
1169            SliderValue::Single(33.5),
1170            "a continuous slider does not snap"
1171        );
1172    }
1173
1174    #[test]
1175    fn a_continuous_slider_steps_by_a_hundredth_of_its_range() {
1176        for step in [0., -1., f32::NAN] {
1177            let state = SliderState::new().min(0.).max(200.).step(step);
1178            assert_eq!(state.accessibility_step(), 2., "step {step}");
1179        }
1180        assert_eq!(SliderState::new().step(5.).accessibility_step(), 5.);
1181    }
1182
1183    #[test]
1184    fn stepping_leaves_an_invalid_value_or_range_alone() {
1185        let nan = step_thumb(SliderValue::Single(f32::NAN), false, 1., 0., 100., 1.);
1186        assert!(nan.end().is_nan(), "a NaN value does not jump to max");
1187        assert_eq!(
1188            step_thumb(SliderValue::Single(50.), false, 1., 100., 0., 1.),
1189            SliderValue::Single(50.),
1190            "min above max"
1191        );
1192        assert_eq!(
1193            step_thumb(SliderValue::Range(60., 20.), true, 1., 0., 100., 1.),
1194            SliderValue::Range(60., 20.),
1195            "a reversed range"
1196        );
1197    }
1198
1199    struct Holder(Entity<SliderState>);
1200
1201    impl Render for Holder {
1202        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1203            div()
1204        }
1205    }
1206
1207    /// A slider of `state`, 100px wide, in a window.
1208    fn sized(
1209        state: SliderState,
1210        cx: &mut gpui::TestAppContext,
1211    ) -> (Entity<SliderState>, &mut gpui::VisualTestContext) {
1212        let (holder, cx) = cx.add_window_view(|_, cx| Holder(cx.new(|_| state)));
1213        let state = holder.read_with(cx, |holder, _| holder.0.clone());
1214        state.update(cx, |state, _| {
1215            state.set_bounds(Bounds::new(
1216                gpui::point(px(0.), px(0.)),
1217                gpui::size(px(100.), px(10.)),
1218            ));
1219        });
1220        (state, cx)
1221    }
1222
1223    fn drag(state: &Entity<SliderState>, x: f32, start: bool, cx: &mut gpui::VisualTestContext) {
1224        state.update_in(cx, |state, window, cx| {
1225            let position = gpui::point(px(x), px(0.));
1226            state.update_value_by_position(Axis::Horizontal, position, start, window, cx);
1227        })
1228    }
1229
1230    #[gpui::test]
1231    fn a_drag_snaps_to_steps_counted_from_min(cx: &mut gpui::TestAppContext) {
1232        let (state, cx) = sized(SliderState::new().min(3.).max(103.).step(5.), cx);
1233        // 9 is between the steps 8 and 13, not next to 10.
1234        drag(&state, 6., false, cx);
1235        assert_eq!(
1236            state.read_with(cx, |state, _| state.value()),
1237            SliderValue::Single(8.)
1238        );
1239    }
1240
1241    #[gpui::test]
1242    fn a_dropped_thumb_stands_at_its_value(cx: &mut gpui::TestAppContext) {
1243        let (state, cx) = sized(SliderState::new().step(25.), cx);
1244        drag(&state, 30., false, cx);
1245        state.read_with(cx, |state, _| {
1246            assert_eq!(state.value(), SliderValue::Single(25.));
1247            assert_eq!(state.percentage(), 0.0..0.25);
1248        });
1249
1250        state.update_in(cx, |state, window, cx| {
1251            state.set_value((25., 75.), window, cx);
1252        });
1253        drag(&state, 40., true, cx);
1254        drag(&state, 60., false, cx);
1255        state.read_with(cx, |state, _| {
1256            assert_eq!(state.value(), SliderValue::Range(50., 50.));
1257            assert_eq!(state.percentage(), 0.5..0.5);
1258        });
1259    }
1260
1261    #[gpui::test]
1262    fn a_zero_step_drags_continuously(cx: &mut gpui::TestAppContext) {
1263        for step in [0., -5., f32::NAN, f32::INFINITY] {
1264            let (state, cx) = sized(SliderState::new().step(step), cx);
1265            drag(&state, 30., false, cx);
1266            state.read_with(cx, |state, _| {
1267                let value = state.value().end();
1268                assert!((value - 30.).abs() < 1e-3, "step {step}: {value}");
1269                assert!((state.percentage().end - 0.3).abs() < 1e-5, "step {step}");
1270            });
1271        }
1272    }
1273
1274    #[gpui::test]
1275    fn an_accessibility_step_reports_a_change(cx: &mut gpui::TestAppContext) {
1276        let (state, cx) = sized(SliderState::new().default_value(99.), cx);
1277        let changes = std::rc::Rc::new(std::cell::Cell::new(0));
1278        cx.update(|_, cx| {
1279            let changes = changes.clone();
1280            cx.subscribe(&state, move |_, event: &SliderEvent, _| {
1281                assert!(matches!(event, SliderEvent::Change(_)));
1282                changes.set(changes.get() + 1);
1283            })
1284            .detach();
1285        });
1286        let step = |delta: f32, cx: &mut gpui::VisualTestContext| {
1287            state.update_in(cx, |state, window, cx| {
1288                state.step_thumb(false, delta, window, cx)
1289            })
1290        };
1291        step(1., cx);
1292        assert_eq!(changes.get(), 1);
1293        assert_eq!(state.read_with(cx, |state, _| state.value().end()), 100.);
1294        // At its end the step changes nothing and reports nothing.
1295        step(1., cx);
1296        assert_eq!(changes.get(), 1);
1297        step(-1., cx);
1298        assert_eq!(changes.get(), 2);
1299    }
1300
1301    #[gpui::test]
1302    fn an_accessibility_step_leaves_an_invalid_value_alone(cx: &mut gpui::TestAppContext) {
1303        let cases = [
1304            SliderState::new().default_value(f32::NAN),
1305            SliderState::new().default_value((60., 20.)),
1306        ];
1307        for state in cases {
1308            let (state, cx) = sized(state, cx);
1309            let before = state.read_with(cx, |state, _| state.value());
1310            let changes = std::rc::Rc::new(std::cell::Cell::new(0));
1311            cx.update(|_, cx| {
1312                let changes = changes.clone();
1313                cx.subscribe(&state, move |_, _: &SliderEvent, _| {
1314                    changes.set(changes.get() + 1)
1315                })
1316                .detach();
1317            });
1318            for start in [false, true] {
1319                state.update_in(cx, |state, window, cx| {
1320                    state.step_thumb(start, 1., window, cx)
1321                });
1322            }
1323            let after = state.read_with(cx, |state, _| state.value());
1324            assert_eq!(
1325                (after.start().to_bits(), after.end().to_bits()),
1326                (before.start().to_bits(), before.end().to_bits()),
1327                "{before:?} became {after:?}"
1328            );
1329            assert_eq!(changes.get(), 0, "{before:?} reported a change");
1330        }
1331    }
1332
1333    #[gpui::test]
1334    fn an_accessibility_step_keeps_a_range(cx: &mut gpui::TestAppContext) {
1335        let (state, cx) = sized(SliderState::new().default_value((20., 60.)), cx);
1336        let step = |start: bool, delta: f32, cx: &mut gpui::VisualTestContext| {
1337            state.update_in(cx, |state, window, cx| {
1338                state.step_thumb(start, delta, window, cx)
1339            });
1340            state.read_with(cx, |state, _| state.value())
1341        };
1342        assert_eq!(step(false, 1., cx), SliderValue::Range(20., 61.));
1343        assert_eq!(step(true, 5., cx), SliderValue::Range(25., 61.));
1344        assert_eq!(
1345            step(true, 100., cx),
1346            SliderValue::Range(61., 61.),
1347            "a thumb stops at the other"
1348        );
1349    }
1350
1351    #[gpui::test]
1352    fn a_continuous_slider_reports_a_usable_step(cx: &mut gpui::TestAppContext) {
1353        use gpui::{Element as _, accesskit, canvas};
1354        use std::{cell::Cell, rc::Rc};
1355
1356        struct Probe(Rc<Cell<Option<f64>>>);
1357
1358        impl Render for Probe {
1359            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1360                let reported = self.0.clone();
1361                canvas(
1362                    move |_, window, cx| {
1363                        let state = cx.new(|_| SliderState::new().step(0.));
1364                        let element = Slider::new(&state).render(window, cx).into_element();
1365                        let mut node = accesskit::Node::new(Role::Slider);
1366                        element.write_a11y_info(&mut node);
1367                        reported.set(node.numeric_value_step());
1368                    },
1369                    |_, _, _, _| {},
1370                )
1371            }
1372        }
1373
1374        let reported = Rc::new(Cell::new(None));
1375        let (_, cx) = cx.add_window_view({
1376            let reported = reported.clone();
1377            move |_, _| Probe(reported)
1378        });
1379        cx.update(|window, cx| window.draw(cx).clear(cx));
1380        assert_eq!(reported.get(), Some(1.), "a hundredth of 0..100");
1381    }
1382
1383    #[test]
1384    #[should_panic(expected = "`min` must be greater than 0")]
1385    fn legacy_logarithmic_validation_is_preserved() {
1386        let _ = SliderState::new().scale(SliderScale::Logarithmic);
1387    }
1388
1389    /// A slider's bar beside a view that reads its state, under retained views.
1390    #[cfg(feature = "gpui-fast")]
1391    mod retained {
1392        use std::{cell::Cell, rc::Rc};
1393
1394        use gpui::{
1395            AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
1396            TestAppContext, Window, div, px,
1397        };
1398
1399        use super::super::{SliderIndicator, SliderState};
1400
1401        /// A view that shows a slider's value, counting its renders.
1402        struct Readout {
1403            state: Entity<SliderState>,
1404            renders: Rc<Cell<usize>>,
1405        }
1406
1407        impl Render for Readout {
1408            fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1409                self.renders.set(self.renders.get() + 1);
1410                div().child(self.state.read(cx).value().to_string())
1411            }
1412        }
1413
1414        /// A slider's bar in a view of its own.
1415        struct Bar {
1416            state: Entity<SliderState>,
1417        }
1418
1419        impl Render for Bar {
1420            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1421                div()
1422                    .w(px(120.))
1423                    .h(px(24.))
1424                    .child(SliderIndicator::new(&self.state).size_full())
1425            }
1426        }
1427
1428        /// The bar and the readout side by side.
1429        struct Pair {
1430            bar: Entity<Bar>,
1431            readout: Entity<Readout>,
1432        }
1433
1434        impl Render for Pair {
1435            fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
1436                div()
1437                    .flex()
1438                    .child(self.bar.clone())
1439                    .child(self.readout.clone())
1440            }
1441        }
1442
1443        /// Drawing a slider's bar again where it was changes nothing in the
1444        /// slider's state: a view beside it that reads the state is not built
1445        /// again for it.
1446        #[gpui::test]
1447        fn drawing_a_slider_again_leaves_its_readers_retained(cx: &mut TestAppContext) {
1448            let renders = Rc::new(Cell::new(0));
1449            let (view, cx) = cx.add_window_view({
1450                let renders = renders.clone();
1451                move |_, cx| {
1452                    let state = cx.new(|_| SliderState::new());
1453                    Pair {
1454                        bar: cx.new(|_| Bar {
1455                            state: state.clone(),
1456                        }),
1457                        readout: cx.new(|_| Readout { state, renders }),
1458                    }
1459                }
1460            });
1461            cx.update(|window, cx| window.draw(cx).clear(cx));
1462            cx.update(|window, cx| window.draw(cx).clear(cx));
1463            let rendered = renders.get();
1464            for _ in 0..3 {
1465                view.update(cx, |pair, cx| pair.bar.update(cx, |_, cx| cx.notify()));
1466                cx.update(|window, cx| window.draw(cx).clear(cx));
1467            }
1468            cx.update(|window, cx| window.draw(cx).clear(cx));
1469            assert_eq!(renders.get(), rendered, "the readout was built again");
1470        }
1471    }
1472}