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, HitboxBehavior, InteractiveElement,
7 IntoElement, MouseButton, MouseDownEvent, Orientation, ParentElement, Pixels, Point, Render,
8 RenderOnce, Role, SharedString, StatefulInteractiveElement, StyleRefinement, Styled,
9 TouchDragEvent, TouchPhase, Window, div, prelude::FluentBuilder as _, px,
10};
11
12use crate::{a11y_disabled::A11yDisabledExt as _, element_ext::ElementExt, geometry::AxisExt};
13
14pub enum SliderEvent {
16 Change(SliderValue),
18 Release(SliderValue),
20}
21
22#[derive(Clone, Copy, Debug, PartialEq)]
29pub enum SliderValue {
30 Single(f32),
31 Range(f32, f32),
32}
33
34impl std::fmt::Display for SliderValue {
35 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36 match self {
37 SliderValue::Single(value) => write!(f, "{}", value),
38 SliderValue::Range(start, end) => write!(f, "{}..{}", start, end),
39 }
40 }
41}
42
43impl From<f32> for SliderValue {
44 fn from(value: f32) -> Self {
45 SliderValue::Single(value)
46 }
47}
48
49impl From<(f32, f32)> for SliderValue {
50 fn from(value: (f32, f32)) -> Self {
51 SliderValue::Range(value.0, value.1)
52 }
53}
54
55impl From<Range<f32>> for SliderValue {
56 fn from(value: Range<f32>) -> Self {
57 SliderValue::Range(value.start, value.end)
58 }
59}
60
61impl Default for SliderValue {
62 fn default() -> Self {
63 SliderValue::Single(0.)
64 }
65}
66
67impl SliderValue {
68 pub fn clamp(self, min: f32, max: f32) -> Self {
70 match self {
71 SliderValue::Single(value) => SliderValue::Single(value.clamp(min, max)),
72 SliderValue::Range(start, end) => {
73 SliderValue::Range(start.clamp(min, max), end.clamp(min, max))
74 }
75 }
76 }
77
78 #[inline]
80 pub fn is_single(&self) -> bool {
81 matches!(self, SliderValue::Single(_))
82 }
83
84 #[inline]
86 pub fn is_range(&self) -> bool {
87 matches!(self, SliderValue::Range(_, _))
88 }
89
90 pub fn start(&self) -> f32 {
92 match self {
93 SliderValue::Single(value) => *value,
94 SliderValue::Range(start, _) => *start,
95 }
96 }
97
98 pub fn end(&self) -> f32 {
100 match self {
101 SliderValue::Single(value) => *value,
102 SliderValue::Range(_, end) => *end,
103 }
104 }
105
106 fn set_start(&mut self, value: f32) {
107 if let SliderValue::Range(_, end) = self {
108 *self = SliderValue::Range(value.min(*end), *end);
109 } else {
110 *self = SliderValue::Single(value);
111 }
112 }
113
114 fn set_end(&mut self, value: f32) {
115 if let SliderValue::Range(start, _) = self {
116 *self = SliderValue::Range(*start, value.max(*start));
117 } else {
118 *self = SliderValue::Single(value);
119 }
120 }
121}
122
123#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
125pub enum SliderScale {
126 #[default]
129 Linear,
130 Logarithmic,
155}
156
157impl SliderScale {
158 #[inline]
159 pub fn is_linear(&self) -> bool {
160 matches!(self, SliderScale::Linear)
161 }
162
163 #[inline]
164 pub fn is_logarithmic(&self) -> bool {
165 matches!(self, SliderScale::Logarithmic)
166 }
167}
168
169pub struct SliderState {
171 min: f32,
172 max: f32,
173 step: f32,
174 value: SliderValue,
175 percentage: Range<f32>,
177 bounds: Bounds<Pixels>,
179 scale: SliderScale,
180 dragging: bool,
183 touch_drag: Option<TouchDrag>,
185}
186
187#[derive(Clone, Copy)]
189struct TouchDrag {
190 start_position: Point<Pixels>,
192 is_start: bool,
194}
195
196impl SliderState {
197 pub fn new() -> Self {
199 Self {
200 min: 0.0,
201 max: 100.0,
202 step: 1.0,
203 value: SliderValue::default(),
204 percentage: (0.0..0.0),
205 bounds: Bounds::default(),
206 scale: SliderScale::default(),
207 dragging: false,
208 touch_drag: None,
209 }
210 }
211
212 pub fn min(mut self, min: f32) -> Self {
214 if self.scale.is_logarithmic() {
215 assert!(
216 min > 0.0,
217 "`min` must be greater than 0 for SliderScale::Logarithmic"
218 );
219 assert!(
220 min < self.max,
221 "`min` must be less than `max` for Logarithmic scale"
222 );
223 }
224 self.min = min;
225 self.update_thumb_pos();
226 self
227 }
228
229 pub fn max(mut self, max: f32) -> Self {
231 if self.scale.is_logarithmic() {
232 assert!(
233 max > self.min,
234 "`max` must be greater than `min` for Logarithmic scale"
235 );
236 }
237 self.max = max;
238 self.update_thumb_pos();
239 self
240 }
241
242 pub fn step(mut self, step: f32) -> Self {
244 self.step = step;
245 self
246 }
247
248 pub fn scale(mut self, scale: SliderScale) -> Self {
250 if scale.is_logarithmic() {
251 assert!(
252 self.min > 0.0,
253 "`min` must be greater than 0 for Logarithmic scale"
254 );
255 assert!(
256 self.max > self.min,
257 "`max` must be greater than `min` for Logarithmic scale"
258 );
259 }
260 self.scale = scale;
261 self.update_thumb_pos();
262 self
263 }
264
265 pub fn default_value(mut self, value: impl Into<SliderValue>) -> Self {
267 self.value = value.into();
268 self.update_thumb_pos();
269 self
270 }
271
272 pub fn set_value(
274 &mut self,
275 value: impl Into<SliderValue>,
276 _: &mut Window,
277 cx: &mut Context<Self>,
278 ) {
279 self.value = value.into();
280 self.update_thumb_pos();
281 cx.notify();
282 }
283
284 pub fn value(&self) -> SliderValue {
286 self.value
287 }
288
289 pub fn min_value(&self) -> f32 {
291 self.min
292 }
293
294 pub fn max_value(&self) -> f32 {
296 self.max
297 }
298
299 pub fn step_value(&self) -> f32 {
301 self.step
302 }
303
304 fn percentage_to_value(&self, percentage: f32) -> f32 {
307 match self.scale {
308 SliderScale::Linear => self.min + (self.max - self.min) * percentage,
309 SliderScale::Logarithmic => {
310 let base = self.max / self.min;
314 (base.powf(percentage) * self.min).clamp(self.min, self.max)
315 }
316 }
317 }
318
319 fn value_to_percentage(&self, value: f32) -> f32 {
322 match self.scale {
323 SliderScale::Linear => {
324 let range = self.max - self.min;
325 if range <= 0.0 {
326 0.0
327 } else {
328 (value - self.min) / range
329 }
330 }
331 SliderScale::Logarithmic => {
332 let base = self.max / self.min;
333 (value / self.min).log(base).clamp(0.0, 1.0)
334 }
335 }
336 }
337
338 fn update_thumb_pos(&mut self) {
339 match self.value {
340 SliderValue::Single(value) => {
341 let percentage = self.value_to_percentage(value.clamp(self.min, self.max));
342 self.percentage = 0.0..percentage;
343 }
344 SliderValue::Range(start, end) => {
345 let clamped_start = start.clamp(self.min, self.max);
346 let clamped_end = end.clamp(self.min, self.max);
347 self.percentage =
348 self.value_to_percentage(clamped_start)..self.value_to_percentage(clamped_end);
349 }
350 }
351 }
352
353 #[doc(hidden)]
355 pub fn update_value_by_position(
356 &mut self,
357 axis: Axis,
358 position: Point<Pixels>,
359 is_start: bool,
360 _: &mut Window,
361 cx: &mut Context<Self>,
362 ) {
363 self.dragging = true;
364 let bounds = self.bounds;
365 let step = self.step;
366
367 let inner_pos = if axis.is_horizontal() {
368 position.x - bounds.left()
369 } else {
370 bounds.bottom() - position.y
371 };
372 let total_size = bounds.size.along(axis);
373 let percentage = inner_pos.clamp(px(0.), total_size) / total_size;
374
375 let percentage = if is_start {
376 percentage.clamp(0.0, self.percentage.end)
377 } else {
378 percentage.clamp(self.percentage.start, 1.0)
379 };
380
381 let value = self.percentage_to_value(percentage);
382 let value = (value / step).round() * step;
383
384 if is_start {
385 self.percentage.start = percentage;
386 self.value.set_start(value);
387 } else {
388 self.percentage.end = percentage;
389 self.value.set_end(value);
390 }
391 cx.emit(SliderEvent::Change(self.value));
392 cx.notify();
393 }
394
395 fn is_nearer_start(&self, axis: Axis, position: Point<Pixels>) -> bool {
397 let size = self.bounds.size.along(axis);
398 let along = if axis.is_horizontal() {
399 position.x - self.bounds.left()
400 } else {
401 self.bounds.bottom() - position.y
402 };
403 let center =
404 ((self.percentage.end - self.percentage.start) / 2. + self.percentage.start) * size;
405 along < center
406 }
407
408 #[doc(hidden)]
411 pub fn handle_release(&mut self, cx: &mut Context<Self>) {
412 if !self.dragging {
413 return;
414 }
415 self.dragging = false;
416 cx.emit(SliderEvent::Release(self.value));
417 }
418}
419
420#[derive(Clone)]
421struct DragThumb((EntityId, bool));
422
423impl Render for DragThumb {
424 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
425 Empty
426 }
427}
428
429#[derive(Clone)]
430struct DragSlider(EntityId);
431
432impl Render for DragSlider {
433 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
434 Empty
435 }
436}
437
438#[derive(IntoElement)]
442pub struct Slider {
443 state: Entity<SliderState>,
444 axis: Axis,
445 disabled: bool,
446 accessibility_label: Option<SharedString>,
447 base: Div,
448 children: Vec<AnyElement>,
449}
450
451impl Slider {
452 pub fn new(state: &Entity<SliderState>) -> Self {
453 Self {
454 state: state.clone(),
455 axis: Axis::Horizontal,
456 disabled: false,
457 accessibility_label: None,
458 base: div(),
459 children: Vec::new(),
460 }
461 }
462
463 pub fn horizontal(mut self) -> Self {
464 self.axis = Axis::Horizontal;
465 self
466 }
467
468 pub fn vertical(mut self) -> Self {
469 self.axis = Axis::Vertical;
470 self
471 }
472
473 pub fn axis(mut self, axis: Axis) -> Self {
474 self.axis = axis;
475 self
476 }
477
478 pub fn disabled(mut self, disabled: bool) -> Self {
479 self.disabled = disabled;
480 self
481 }
482
483 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
485 self.accessibility_label = Some(label.into());
486 self
487 }
488}
489
490impl ParentElement for Slider {
491 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
492 self.children.extend(elements);
493 }
494}
495
496impl Styled for Slider {
497 fn style(&mut self) -> &mut StyleRefinement {
498 self.base.style()
499 }
500}
501
502impl RenderOnce for Slider {
503 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
504 let axis = self.axis;
505 let entity_id = self.state.entity_id();
506 let state = self.state.read(cx);
507 let slider_state = self.state.clone();
508
509 self.base
510 .id(("slider", entity_id))
511 .test_support()
512 .role(Role::Slider)
513 .when_some(self.accessibility_label, |this, label| {
514 this.aria_label(label)
515 })
516 .aria_numeric_value(state.value().end() as f64)
517 .aria_min_numeric_value(state.min_value() as f64)
518 .aria_max_numeric_value(state.max_value() as f64)
519 .aria_numeric_value_step(state.step_value() as f64)
520 .aria_orientation(if axis.is_vertical() {
521 Orientation::Vertical
522 } else {
523 Orientation::Horizontal
524 })
525 .when(!self.disabled, |this| {
528 this.on_a11y_action(AccessibleAction::Increment, {
529 let state = slider_state.clone();
530 move |_, window, cx| {
531 state.update(cx, |state, cx| {
532 let value =
533 (state.value().end() + state.step_value()).min(state.max_value());
534 state.set_value(value, window, cx);
535 });
536 }
537 })
538 .on_a11y_action(AccessibleAction::Decrement, {
539 let state = slider_state.clone();
540 move |_, window, cx| {
541 state.update(cx, |state, cx| {
542 let value =
543 (state.value().end() - state.step_value()).max(state.min_value());
544 state.set_value(value, window, cx);
545 });
546 }
547 })
548 .on_mouse_up(
549 MouseButton::Left,
550 window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
551 )
552 .on_mouse_up_out(
553 MouseButton::Left,
554 window.listener_for(&self.state, |state, _, _, cx| state.handle_release(cx)),
555 )
556 })
557 .children(self.children)
558 .a11y_disabled(self.disabled)
559 }
560}
561
562#[derive(IntoElement)]
564pub struct SliderTrack {
565 state: Entity<SliderState>,
566 axis: Axis,
567 disabled: bool,
568 base: Div,
569 children: Vec<AnyElement>,
570}
571
572impl SliderTrack {
573 pub fn new(state: &Entity<SliderState>) -> Self {
574 Self {
575 state: state.clone(),
576 axis: Axis::Horizontal,
577 disabled: false,
578 base: div(),
579 children: Vec::new(),
580 }
581 }
582
583 pub fn axis(mut self, axis: Axis) -> Self {
584 self.axis = axis;
585 self
586 }
587 pub fn disabled(mut self, disabled: bool) -> Self {
588 self.disabled = disabled;
589 self
590 }
591}
592
593impl ParentElement for SliderTrack {
594 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
595 self.children.extend(elements);
596 }
597}
598
599impl Styled for SliderTrack {
600 fn style(&mut self) -> &mut StyleRefinement {
601 self.base.style()
602 }
603}
604
605impl InteractiveElement for SliderTrack {
606 fn interactivity(&mut self) -> &mut gpui::Interactivity {
607 self.base.interactivity()
608 }
609}
610
611impl RenderOnce for SliderTrack {
612 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
613 let axis = self.axis;
614 let entity_id = self.state.entity_id();
615 let state = self.state.read(cx);
616 let is_range = state.value().is_range();
617 let percentage = state.percentage();
618 let touch_layer = (!self.disabled).then(|| {
622 let slider_state = self.state.clone();
623 gpui::canvas(
624 |bounds, window, _| window.insert_hitbox(bounds, HitboxBehavior::Normal),
625 move |_, hitbox, window, _| {
626 let slider_state = slider_state.clone();
627 window.on_mouse_event(move |event: &TouchDragEvent, phase, window, cx| {
628 if !phase.bubble() {
629 return;
630 }
631 slider_state.update(cx, |state, cx| {
632 if event.phase == TouchPhase::Started {
633 if state.touch_drag.take().is_some() {
636 state.handle_release(cx);
637 }
638 if window.default_prevented() || !hitbox.is_hovered(window) {
641 return;
642 }
643 window.prevent_default();
644 let is_start =
645 is_range && state.is_nearer_start(axis, event.start_position);
646 state.touch_drag = Some(TouchDrag {
647 start_position: event.start_position,
648 is_start,
649 });
650 }
651 let Some(drag) = state.touch_drag else {
652 return;
653 };
654 if drag.start_position != event.start_position {
655 return;
656 }
657 cx.stop_propagation();
658 match event.phase {
659 TouchPhase::Started | TouchPhase::Moved => {
660 state.update_value_by_position(
661 axis,
662 event.position,
663 drag.is_start,
664 window,
665 cx,
666 );
667 }
668 TouchPhase::Ended | TouchPhase::Cancelled => {
669 state.touch_drag = None;
670 state.handle_release(cx);
671 }
672 }
673 });
674 });
675 },
676 )
677 .absolute()
678 .size_full()
679 });
680 self.base
681 .id("slider-bar-container")
682 .test_support()
683 .children(self.children)
684 .children(touch_layer)
685 .when(!self.disabled, |this| {
686 this.on_mouse_down(
687 MouseButton::Left,
688 window.listener_for(
689 &self.state,
690 move |state, event: &MouseDownEvent, window, cx| {
691 let is_start = if is_range {
692 let size = state.bounds().size.along(axis);
693 let position = if axis.is_horizontal() {
694 event.position.x - state.bounds().left()
695 } else {
696 state.bounds().bottom() - event.position.y
697 };
698 let center = ((percentage.end - percentage.start) / 2.
699 + percentage.start)
700 * size;
701 position < center
702 } else {
703 false
704 };
705 state.update_value_by_position(
706 axis,
707 event.position,
708 is_start,
709 window,
710 cx,
711 );
712 },
713 ),
714 )
715 .when(!is_range, |this| {
716 this.on_drag(DragSlider(entity_id), |drag, _, _, cx| {
717 cx.stop_propagation();
718 cx.new(|_| drag.clone())
719 })
720 .on_drag_move(window.listener_for(
721 &self.state,
722 move |state, event: &DragMoveEvent<DragSlider>, window, cx| {
723 let DragSlider(id) = event.drag(cx);
724 if *id == entity_id {
725 state.update_value_by_position(
726 axis,
727 event.event.position,
728 false,
729 window,
730 cx,
731 );
732 }
733 },
734 ))
735 })
736 })
737 }
738}
739
740#[derive(IntoElement)]
742pub struct SliderIndicator {
743 state: Entity<SliderState>,
744 base: Div,
745 children: Vec<AnyElement>,
746}
747
748impl SliderIndicator {
749 pub fn new(state: &Entity<SliderState>) -> Self {
750 Self {
751 state: state.clone(),
752 base: div(),
753 children: Vec::new(),
754 }
755 }
756}
757
758impl ParentElement for SliderIndicator {
759 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
760 self.children.extend(elements);
761 }
762}
763
764impl Styled for SliderIndicator {
765 fn style(&mut self) -> &mut StyleRefinement {
766 self.base.style()
767 }
768}
769
770impl InteractiveElement for SliderIndicator {
771 fn interactivity(&mut self) -> &mut gpui::Interactivity {
772 self.base.interactivity()
773 }
774}
775
776impl StatefulInteractiveElement for SliderIndicator {}
777
778impl RenderOnce for SliderIndicator {
779 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
780 self.base
781 .id("slider-bar")
782 .children(self.children)
783 .on_prepaint({
784 let state = self.state;
785 move |bounds, _, cx| {
789 if state.read(cx).bounds != bounds {
790 state.update(cx, |state, _| state.set_bounds(bounds));
791 }
792 }
793 })
794 }
795}
796
797#[derive(IntoElement)]
799pub struct SliderThumb {
800 state: Entity<SliderState>,
801 axis: Axis,
802 start: bool,
803 disabled: bool,
804 base: Div,
805 children: Vec<AnyElement>,
806}
807
808impl SliderThumb {
809 pub fn new(state: &Entity<SliderState>) -> Self {
810 Self {
811 state: state.clone(),
812 axis: Axis::Horizontal,
813 start: false,
814 disabled: false,
815 base: div(),
816 children: Vec::new(),
817 }
818 }
819
820 pub fn axis(mut self, axis: Axis) -> Self {
821 self.axis = axis;
822 self
823 }
824 pub fn start(mut self, start: bool) -> Self {
825 self.start = start;
826 self
827 }
828 pub fn disabled(mut self, disabled: bool) -> Self {
829 self.disabled = disabled;
830 self
831 }
832}
833
834impl ParentElement for SliderThumb {
835 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
836 self.children.extend(elements);
837 }
838}
839
840impl Styled for SliderThumb {
841 fn style(&mut self) -> &mut StyleRefinement {
842 self.base.style()
843 }
844}
845
846impl InteractiveElement for SliderThumb {
847 fn interactivity(&mut self) -> &mut gpui::Interactivity {
848 self.base.interactivity()
849 }
850}
851
852impl StatefulInteractiveElement for SliderThumb {}
853
854impl RenderOnce for SliderThumb {
855 fn render(self, window: &mut Window, _: &mut App) -> impl IntoElement {
856 let entity_id = self.state.entity_id();
857 let axis = self.axis;
858 let start = self.start;
859 self.base
860 .id(("slider-thumb", start as u32))
861 .test_support()
862 .children(self.children)
863 .when(!self.disabled, |this| {
864 this.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
865 .on_drag(DragThumb((entity_id, start)), |drag, _, _, cx| {
866 cx.stop_propagation();
867 cx.new(|_| drag.clone())
868 })
869 .on_drag_move(window.listener_for(
870 &self.state,
871 move |state, event: &DragMoveEvent<DragThumb>, window, cx| {
872 let DragThumb((id, start)) = event.drag(cx);
873 if *id == entity_id {
874 state.update_value_by_position(
875 axis,
876 event.event.position,
877 *start,
878 window,
879 cx,
880 );
881 }
882 },
883 ))
884 })
885 }
886}
887
888impl EventEmitter<SliderEvent> for SliderState {}
889
890impl SliderState {
891 #[doc(hidden)]
892 pub fn percentage(&self) -> Range<f32> {
893 self.percentage.clone()
894 }
895
896 #[doc(hidden)]
897 pub fn bounds(&self) -> Bounds<Pixels> {
898 self.bounds
899 }
900
901 #[doc(hidden)]
902 pub fn set_bounds(&mut self, bounds: Bounds<Pixels>) {
903 self.bounds = bounds;
904 }
905}
906
907#[cfg(test)]
908mod tests {
909 use super::*;
910
911 #[test]
912 fn legacy_value_conversions_and_clamping_are_preserved() {
913 assert_eq!(SliderValue::from(5.), SliderValue::Single(5.));
914 assert_eq!(SliderValue::from((2., 8.)), SliderValue::Range(2., 8.));
915 assert_eq!(SliderValue::from(2.0..8.0), SliderValue::Range(2., 8.));
916 assert_eq!(
917 SliderValue::Range(-1., 12.).clamp(0., 10.),
918 SliderValue::Range(0., 10.)
919 );
920 }
921
922 #[test]
923 fn legacy_linear_state_keeps_percentage_and_range_ordering() {
924 let state = SliderState::new()
925 .min(0.)
926 .max(200.)
927 .default_value((50., 150.));
928 assert_eq!(state.value(), SliderValue::Range(50., 150.));
929 assert_eq!(state.percentage(), 0.25..0.75);
930 }
931
932 #[test]
933 fn legacy_logarithmic_state_keeps_mapping() {
934 let state = SliderState::new()
935 .min(1.)
936 .max(1000.)
937 .scale(SliderScale::Logarithmic)
938 .default_value(10.);
939 let percentage = state.percentage().end;
940 assert!((percentage - (1. / 3.)).abs() < 0.0001);
941 }
942
943 #[test]
944 #[should_panic(expected = "`min` must be greater than 0")]
945 fn legacy_logarithmic_validation_is_preserved() {
946 let _ = SliderState::new().scale(SliderScale::Logarithmic);
947 }
948
949 #[cfg(feature = "gpui-fast")]
951 mod retained {
952 use std::{cell::Cell, rc::Rc};
953
954 use gpui::{
955 AppContext as _, Context, Entity, IntoElement, ParentElement as _, Render, Styled as _,
956 TestAppContext, Window, div, px,
957 };
958
959 use super::super::{SliderIndicator, SliderState};
960
961 struct Readout {
963 state: Entity<SliderState>,
964 renders: Rc<Cell<usize>>,
965 }
966
967 impl Render for Readout {
968 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
969 self.renders.set(self.renders.get() + 1);
970 div().child(self.state.read(cx).value().to_string())
971 }
972 }
973
974 struct Bar {
976 state: Entity<SliderState>,
977 }
978
979 impl Render for Bar {
980 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
981 div()
982 .w(px(120.))
983 .h(px(24.))
984 .child(SliderIndicator::new(&self.state).size_full())
985 }
986 }
987
988 struct Pair {
990 bar: Entity<Bar>,
991 readout: Entity<Readout>,
992 }
993
994 impl Render for Pair {
995 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
996 div()
997 .flex()
998 .child(self.bar.clone())
999 .child(self.readout.clone())
1000 }
1001 }
1002
1003 #[gpui::test]
1007 fn drawing_a_slider_again_leaves_its_readers_retained(cx: &mut TestAppContext) {
1008 let renders = Rc::new(Cell::new(0));
1009 let (view, cx) = cx.add_window_view({
1010 let renders = renders.clone();
1011 move |_, cx| {
1012 let state = cx.new(|_| SliderState::new());
1013 Pair {
1014 bar: cx.new(|_| Bar {
1015 state: state.clone(),
1016 }),
1017 readout: cx.new(|_| Readout { state, renders }),
1018 }
1019 }
1020 });
1021 cx.update(|window, cx| window.draw(cx).clear(cx));
1022 cx.update(|window, cx| window.draw(cx).clear(cx));
1023 let rendered = renders.get();
1024 for _ in 0..3 {
1025 view.update(cx, |pair, cx| pair.bar.update(cx, |_, cx| cx.notify()));
1026 cx.update(|window, cx| window.draw(cx).clear(cx));
1027 }
1028 cx.update(|window, cx| window.draw(cx).clear(cx));
1029 assert_eq!(renders.get(), rendered, "the readout was built again");
1030 }
1031 }
1032}