cadmus_core/view/
slider.rs

1use super::{
2    Bus, Event, Hub, Id, RenderData, RenderQueue, SliderId, View, ID_FEEDER, THICKNESS_SMALL,
3};
4use crate::color::{BLACK, PROGRESS_EMPTY, PROGRESS_FULL, PROGRESS_VALUE, WHITE};
5use crate::context::Context;
6use crate::device::CURRENT_DEVICE;
7use crate::font::{font_from_style, Fonts, SLIDER_VALUE};
8use crate::framebuffer::{Framebuffer, UpdateMode};
9use crate::geom::{halves, BorderSpec, CornerSpec, Rectangle};
10use crate::input::{DeviceEvent, FingerStatus};
11use crate::unit::scale_by_dpi;
12
13const PROGRESS_HEIGHT: f32 = 7.0;
14const BUTTON_DIAMETER: f32 = 46.0;
15
16pub struct Slider {
17    id: Id,
18    rect: Rectangle,
19    children: Vec<Box<dyn View>>,
20    slider_id: SliderId,
21    value: f32,
22    min_value: f32,
23    max_value: f32,
24    active: bool,
25    last_x: i32,
26}
27
28impl Slider {
29    pub fn new(
30        rect: Rectangle,
31        slider_id: SliderId,
32        value: f32,
33        min_value: f32,
34        max_value: f32,
35    ) -> Slider {
36        Slider {
37            id: ID_FEEDER.next(),
38            rect,
39            children: Vec::new(),
40            slider_id,
41            value,
42            min_value,
43            max_value,
44            active: false,
45            last_x: -1,
46        }
47    }
48
49    pub fn update_value(&mut self, x_hit: i32) {
50        let dpi = CURRENT_DEVICE.dpi;
51        let button_diameter = scale_by_dpi(BUTTON_DIAMETER, dpi) as i32;
52        let (small_radius, big_radius) = halves(button_diameter);
53        let x_offset = x_hit
54            .max(self.rect.min.x + small_radius)
55            .min(self.rect.max.x - big_radius);
56        let progress = ((x_offset - self.rect.min.x - small_radius) as f32
57            / (self.rect.width() as i32 - button_diameter) as f32)
58            .clamp(0.0, 1.0);
59        self.value = self.min_value + progress * (self.max_value - self.min_value);
60    }
61
62    pub fn update(&mut self, value: f32, rq: &mut RenderQueue) {
63        if (self.value - value).abs() >= f32::EPSILON {
64            self.value = value;
65            rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
66        }
67    }
68}
69
70impl View for Slider {
71    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
72    fn handle_event(
73        &mut self,
74        evt: &Event,
75        _hub: &Hub,
76        bus: &mut Bus,
77        rq: &mut RenderQueue,
78        _context: &mut Context,
79    ) -> bool {
80        match *evt {
81            Event::Device(DeviceEvent::Finger {
82                status, position, ..
83            }) => match status {
84                FingerStatus::Down if self.rect.includes(position) => {
85                    self.active = true;
86                    self.update_value(position.x);
87                    rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
88                    bus.push_back(Event::Slider(self.slider_id, self.value, status));
89                    self.last_x = position.x;
90                    true
91                }
92                FingerStatus::Motion if self.active && position.x != self.last_x => {
93                    self.update_value(position.x);
94                    rq.add(RenderData::no_wait(
95                        self.id,
96                        self.rect,
97                        UpdateMode::FastMono,
98                    ));
99                    bus.push_back(Event::Slider(self.slider_id, self.value, status));
100                    self.last_x = position.x;
101                    true
102                }
103                FingerStatus::Up if self.active => {
104                    self.active = false;
105                    if position.x != self.last_x {
106                        self.update_value(position.x);
107                        self.last_x = position.x;
108                    }
109                    rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
110                    bus.push_back(Event::Slider(self.slider_id, self.value, status));
111                    true
112                }
113                _ => self.active,
114            },
115            _ => false,
116        }
117    }
118
119    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts, _rect), fields(rect = ?_rect)))]
120    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
121        let dpi = CURRENT_DEVICE.dpi;
122        let progress_height = scale_by_dpi(PROGRESS_HEIGHT, dpi) as i32;
123        let button_diameter = scale_by_dpi(BUTTON_DIAMETER, dpi) as i32;
124        let border_thickness = scale_by_dpi(THICKNESS_SMALL, dpi) as u16;
125
126        let progress = (self.value - self.min_value) / (self.max_value - self.min_value);
127        let (small_radius, big_radius) = halves(button_diameter);
128        let x_offset = self.rect.min.x
129            + small_radius
130            + ((self.rect.width() as f32 - button_diameter as f32) * progress) as i32;
131
132        fb.draw_rectangle(&self.rect, WHITE);
133
134        let (small_mini_radius, big_mini_radius) = halves(progress_height);
135        let (small_padding, big_padding) = halves(self.rect.height() as i32 - progress_height);
136        let rect = rect![
137            self.rect.min.x + small_radius - big_mini_radius,
138            self.rect.min.y + small_padding,
139            self.rect.max.x - big_radius + small_mini_radius,
140            self.rect.max.y - big_padding
141        ];
142
143        fb.draw_rounded_rectangle_with_border(
144            &rect,
145            &CornerSpec::Uniform(small_mini_radius),
146            &BorderSpec {
147                thickness: border_thickness,
148                color: BLACK,
149            },
150            &|x, _| {
151                if x < x_offset {
152                    PROGRESS_FULL
153                } else {
154                    PROGRESS_EMPTY
155                }
156            },
157        );
158
159        let (small_padding, big_padding) = halves(self.rect.height() as i32 - button_diameter);
160        let rect = rect![
161            x_offset - small_radius,
162            self.rect.min.y + small_padding,
163            x_offset + big_radius,
164            self.rect.max.y - big_padding
165        ];
166        let fill_color = if self.active { BLACK } else { WHITE };
167
168        fb.draw_rounded_rectangle_with_border(
169            &rect,
170            &CornerSpec::Uniform(small_radius),
171            &BorderSpec {
172                thickness: 2 * border_thickness,
173                color: BLACK,
174            },
175            &fill_color,
176        );
177
178        let font = font_from_style(fonts, &SLIDER_VALUE, dpi);
179        let plan = font.plan(&format!("{:.1}", self.value), None, None);
180        let x_height = font.x_heights.1 as i32;
181
182        let x_drift = if self.value > (self.min_value + self.max_value) / 2.0 {
183            -(small_radius + plan.width)
184        } else {
185            small_radius
186        };
187
188        let pt = pt!(
189            x_offset + x_drift,
190            self.rect.min.y + x_height.max(small_padding)
191        );
192        font.render(fb, PROGRESS_VALUE, &plan, pt);
193    }
194
195    fn rect(&self) -> &Rectangle {
196        &self.rect
197    }
198
199    fn rect_mut(&mut self) -> &mut Rectangle {
200        &mut self.rect
201    }
202
203    fn children(&self) -> &Vec<Box<dyn View>> {
204        &self.children
205    }
206
207    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
208        &mut self.children
209    }
210
211    fn id(&self) -> Id {
212        self.id
213    }
214}