cadmus_core/view/
frontlight.rs

1use super::button::Button;
2use super::common::shift;
3use super::icon::Icon;
4use super::label::Label;
5use super::presets_list::PresetsList;
6use super::slider::Slider;
7use super::{
8    Align, Bus, EntryId, Event, Hub, Id, RenderData, RenderQueue, SliderId, View, ViewId, ID_FEEDER,
9};
10use super::{BORDER_RADIUS_MEDIUM, SMALL_BAR_HEIGHT, THICKNESS_LARGE};
11use crate::color::{BLACK, WHITE};
12use crate::context::Context;
13use crate::device::CURRENT_DEVICE;
14use crate::font::{font_from_style, Fonts, NORMAL_STYLE};
15use crate::framebuffer::{Framebuffer, UpdateMode};
16use crate::frontlight::LightLevels;
17use crate::geom::{BorderSpec, CornerSpec, Rectangle};
18use crate::gesture::GestureEvent;
19use crate::settings::{guess_frontlight, LightPreset};
20use crate::unit::scale_by_dpi;
21
22const LABEL_SAVE: &str = "Save";
23const LABEL_GUESS: &str = "Guess";
24
25pub struct FrontlightWindow {
26    id: Id,
27    rect: Rectangle,
28    children: Vec<Box<dyn View>>,
29}
30
31impl FrontlightWindow {
32    pub fn new(context: &mut Context) -> FrontlightWindow {
33        let id = ID_FEEDER.next();
34        let fonts = &mut context.fonts;
35        let levels = context.frontlight.levels();
36        let presets = &context.settings.frontlight_presets;
37        let mut children = Vec::new();
38        let dpi = CURRENT_DEVICE.dpi;
39        let (width, height) = context.display.dims;
40        let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
41        let thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as i32;
42        let border_radius = scale_by_dpi(BORDER_RADIUS_MEDIUM, dpi) as i32;
43
44        let (x_height, padding) = {
45            let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
46            (font.x_heights.0 as i32, font.em() as i32)
47        };
48
49        let window_width = width as i32 - 2 * padding;
50
51        let mut window_height = small_height * 3 + 2 * padding;
52
53        if CURRENT_DEVICE.has_natural_light() {
54            window_height += small_height;
55        }
56
57        if !presets.is_empty() {
58            window_height += small_height;
59        }
60
61        let dx = (width as i32 - window_width) / 2;
62        let dy = (height as i32 - window_height) / 3;
63
64        let rect = rect![dx, dy, dx + window_width, dy + window_height];
65
66        let corners = CornerSpec::Detailed {
67            north_west: 0,
68            north_east: border_radius - thickness,
69            south_east: 0,
70            south_west: 0,
71        };
72
73        let close_icon = Icon::new(
74            "close",
75            rect![
76                rect.max.x - small_height,
77                rect.min.y + thickness,
78                rect.max.x - thickness,
79                rect.min.y + small_height
80            ],
81            Event::Close(ViewId::Frontlight),
82        )
83        .corners(Some(corners));
84
85        children.push(Box::new(close_icon) as Box<dyn View>);
86
87        let label = Label::new(
88            rect![
89                rect.min.x + small_height,
90                rect.min.y + thickness,
91                rect.max.x - small_height,
92                rect.min.y + small_height
93            ],
94            "Frontlight".to_string(),
95            Align::Center,
96        );
97
98        children.push(Box::new(label) as Box<dyn View>);
99
100        let mut button_y = rect.min.y + 2 * small_height;
101
102        if CURRENT_DEVICE.has_natural_light() {
103            let max_label_width = {
104                let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
105                ["Intensity", "Warmth"]
106                    .iter()
107                    .map(|t| font.plan(t, None, None).width)
108                    .max()
109                    .unwrap() as i32
110            };
111
112            for (index, slider_id) in [SliderId::LightIntensity, SliderId::LightWarmth]
113                .iter()
114                .enumerate()
115            {
116                let min_y = rect.min.y + (index + 1) as i32 * small_height;
117                let label = Label::new(
118                    rect![
119                        rect.min.x + padding,
120                        min_y,
121                        rect.min.x + 2 * padding + max_label_width,
122                        min_y + small_height
123                    ],
124                    slider_id.label(),
125                    Align::Right(padding / 2),
126                );
127                children.push(Box::new(label) as Box<dyn View>);
128
129                let value = if *slider_id == SliderId::LightIntensity {
130                    levels.intensity
131                } else {
132                    levels.warmth
133                };
134
135                let slider = Slider::new(
136                    rect![
137                        rect.min.x + max_label_width + 3 * padding,
138                        min_y,
139                        rect.max.x - padding,
140                        min_y + small_height
141                    ],
142                    *slider_id,
143                    value,
144                    0.0,
145                    100.0,
146                );
147                children.push(Box::new(slider) as Box<dyn View>);
148            }
149
150            button_y += small_height;
151        } else {
152            let min_y = rect.min.y + small_height;
153            let slider = Slider::new(
154                rect![
155                    rect.min.x + padding,
156                    min_y,
157                    rect.max.x - padding,
158                    min_y + small_height
159                ],
160                SliderId::LightIntensity,
161                levels.intensity,
162                0.0,
163                100.0,
164            );
165            children.push(Box::new(slider) as Box<dyn View>);
166        }
167
168        let max_label_width = {
169            let font = font_from_style(fonts, &NORMAL_STYLE, dpi);
170            [LABEL_SAVE, LABEL_GUESS]
171                .iter()
172                .map(|t| font.plan(t, None, None).width)
173                .max()
174                .unwrap() as i32
175        };
176
177        let button_height = 4 * x_height;
178
179        let button_save = Button::new(
180            rect![
181                rect.min.x + 3 * padding,
182                button_y + small_height - button_height,
183                rect.min.x + 5 * padding + max_label_width,
184                button_y + small_height
185            ],
186            Event::Save,
187            LABEL_SAVE.to_string(),
188        );
189        children.push(Box::new(button_save) as Box<dyn View>);
190
191        let button_guess = Button::new(
192            rect![
193                rect.max.x - 5 * padding - max_label_width,
194                button_y + small_height - button_height,
195                rect.max.x - 3 * padding,
196                button_y + small_height
197            ],
198            Event::Guess,
199            LABEL_GUESS.to_string(),
200        )
201        .disabled(presets.len() < 2);
202        children.push(Box::new(button_guess) as Box<dyn View>);
203
204        if !presets.is_empty() {
205            let presets_rect = rect![
206                rect.min.x + thickness + 4 * padding,
207                rect.max.y - small_height - 2 * padding,
208                rect.max.x - thickness - 4 * padding,
209                rect.max.y - thickness - 2 * padding
210            ];
211            let mut presets_list = PresetsList::new(presets_rect);
212            presets_list.update(presets, &mut RenderQueue::new(), fonts);
213            children.push(Box::new(presets_list) as Box<dyn View>);
214        }
215
216        FrontlightWindow { id, rect, children }
217    }
218
219    fn toggle_presets(&mut self, enable: bool, rq: &mut RenderQueue, context: &mut Context) {
220        let dpi = CURRENT_DEVICE.dpi;
221        let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
222
223        if enable {
224            let thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as i32;
225            let padding = {
226                let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
227                font.em() as i32
228            };
229            shift(self, pt!(0, -(small_height) / 2));
230            self.rect.max.y += small_height;
231            let presets_rect = rect![
232                self.rect.min.x + thickness + 4 * padding,
233                self.rect.max.y - small_height - 2 * padding,
234                self.rect.max.x - thickness - 4 * padding,
235                self.rect.max.y - thickness - 2 * padding
236            ];
237            let mut presets_list = PresetsList::new(presets_rect);
238            presets_list.update(
239                &context.settings.frontlight_presets,
240                &mut RenderQueue::new(),
241                &mut context.fonts,
242            );
243            self.children.push(Box::new(presets_list) as Box<dyn View>);
244            rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
245        } else {
246            self.children.pop();
247            rq.add(RenderData::expose(self.rect, UpdateMode::Gui));
248            shift(self, pt!(0, small_height / 2));
249            self.rect.max.y -= small_height;
250        }
251    }
252
253    fn set_frontlight_levels(
254        &mut self,
255        frontlight_levels: LightLevels,
256        rq: &mut RenderQueue,
257        context: &mut Context,
258    ) {
259        let LightLevels { intensity, warmth } = frontlight_levels;
260        context.frontlight.set_intensity(intensity);
261        context.frontlight.set_warmth(warmth);
262        if CURRENT_DEVICE.has_natural_light() {
263            if let Some(slider_intensity) = self.child_mut(3).downcast_mut::<Slider>() {
264                slider_intensity.update(intensity, rq);
265            }
266            if let Some(slider_warmth) = self.child_mut(5).downcast_mut::<Slider>() {
267                slider_warmth.update(warmth, rq);
268            }
269        } else if let Some(slider_intensity) = self.child_mut(2).downcast_mut::<Slider>() {
270            slider_intensity.update(intensity, rq);
271        }
272    }
273
274    fn update_presets(&mut self, rq: &mut RenderQueue, context: &mut Context) {
275        let len = self.len();
276        if let Some(presets_list) = self.child_mut(len - 1).downcast_mut::<PresetsList>() {
277            presets_list.update(&context.settings.frontlight_presets, rq, &mut context.fonts);
278        }
279    }
280}
281
282impl View for FrontlightWindow {
283    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, hub, _bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
284    fn handle_event(
285        &mut self,
286        evt: &Event,
287        hub: &Hub,
288        _bus: &mut Bus,
289        rq: &mut RenderQueue,
290        context: &mut Context,
291    ) -> bool {
292        match *evt {
293            Event::Slider(SliderId::LightIntensity, value, _) => {
294                context.frontlight.set_intensity(value);
295                true
296            }
297            Event::Slider(SliderId::LightWarmth, value, _) => {
298                context.frontlight.set_warmth(value);
299                true
300            }
301            Event::Gesture(GestureEvent::Tap(center)) if !self.rect.includes(center) => {
302                hub.send(Event::Close(ViewId::Frontlight)).ok();
303                true
304            }
305            Event::Gesture(..) => true,
306            Event::Save => {
307                let lightsensor_level = if CURRENT_DEVICE.has_lightsensor() {
308                    context.lightsensor.level().ok()
309                } else {
310                    None
311                };
312                let light_preset = LightPreset {
313                    lightsensor_level,
314                    frontlight_levels: context.frontlight.levels(),
315                    ..Default::default()
316                };
317                context.settings.frontlight_presets.push(light_preset);
318                context
319                    .settings
320                    .frontlight_presets
321                    .sort_by(|a, b| a.timestamp.cmp(&b.timestamp));
322                if context.settings.frontlight_presets.len() == 1 {
323                    self.toggle_presets(true, rq, context);
324                } else {
325                    if context.settings.frontlight_presets.len() == 2 {
326                        let index = self.len() - 2;
327                        if let Some(button_guess) = self.child_mut(index).downcast_mut::<Button>() {
328                            button_guess.disabled = false;
329                            rq.add(RenderData::new(
330                                button_guess.id(),
331                                *button_guess.rect(),
332                                UpdateMode::Gui,
333                            ));
334                        }
335                    }
336                    self.update_presets(rq, context);
337                }
338                true
339            }
340            Event::Select(EntryId::RemovePreset(index)) => {
341                if index < context.settings.frontlight_presets.len() {
342                    context.settings.frontlight_presets.remove(index);
343                    if context.settings.frontlight_presets.is_empty() {
344                        self.toggle_presets(false, rq, context);
345                    } else {
346                        if context.settings.frontlight_presets.len() == 1 {
347                            let index = self.len() - 2;
348                            if let Some(button_guess) =
349                                self.child_mut(index).downcast_mut::<Button>()
350                            {
351                                button_guess.disabled = true;
352                                rq.add(RenderData::new(
353                                    button_guess.id(),
354                                    *button_guess.rect(),
355                                    UpdateMode::Gui,
356                                ));
357                            }
358                        }
359                        self.update_presets(rq, context);
360                    }
361                }
362                true
363            }
364            Event::LoadPreset(index) => {
365                let frontlight_levels =
366                    context.settings.frontlight_presets[index].frontlight_levels;
367                self.set_frontlight_levels(frontlight_levels, rq, context);
368                true
369            }
370            Event::Guess => {
371                let lightsensor_level = if CURRENT_DEVICE.has_lightsensor() {
372                    context.lightsensor.level().ok()
373                } else {
374                    None
375                };
376                if let Some(ref frontlight_levels) =
377                    guess_frontlight(lightsensor_level, &context.settings.frontlight_presets)
378                {
379                    self.set_frontlight_levels(*frontlight_levels, rq, context);
380                }
381                true
382            }
383            _ => false,
384        }
385    }
386
387    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, _fonts, _rect), fields(rect = ?_rect)))]
388    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {
389        let dpi = CURRENT_DEVICE.dpi;
390
391        let border_radius = scale_by_dpi(BORDER_RADIUS_MEDIUM, dpi) as i32;
392        let border_thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as u16;
393
394        fb.draw_rounded_rectangle_with_border(
395            &self.rect,
396            &CornerSpec::Uniform(border_radius),
397            &BorderSpec {
398                thickness: border_thickness,
399                color: BLACK,
400            },
401            &WHITE,
402        );
403    }
404
405    fn resize(&mut self, _rect: Rectangle, hub: &Hub, rq: &mut RenderQueue, context: &mut Context) {
406        let dpi = CURRENT_DEVICE.dpi;
407        let (width, height) = context.display.dims;
408        let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
409        let thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as i32;
410
411        let (x_height, padding) = {
412            let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
413            (font.x_heights.0 as i32, font.em() as i32)
414        };
415
416        let window_width = width as i32 - 2 * padding;
417
418        let mut window_height = small_height * 3 + 2 * padding;
419
420        if CURRENT_DEVICE.has_natural_light() {
421            window_height += small_height;
422        }
423
424        if !context.settings.frontlight_presets.is_empty() {
425            window_height += small_height;
426        }
427
428        let dx = (width as i32 - window_width) / 2;
429        let dy = (height as i32 - window_height) / 3;
430
431        let rect = rect![dx, dy, dx + window_width, dy + window_height];
432
433        self.children[0].resize(
434            rect![
435                rect.max.x - small_height,
436                rect.min.y + thickness,
437                rect.max.x - thickness,
438                rect.min.y + small_height
439            ],
440            hub,
441            rq,
442            context,
443        );
444        self.children[1].resize(
445            rect![
446                rect.min.x + small_height,
447                rect.min.y + thickness,
448                rect.max.x - small_height,
449                rect.min.y + small_height
450            ],
451            hub,
452            rq,
453            context,
454        );
455
456        let mut button_y = rect.min.y + 2 * small_height;
457        let mut index = 2;
458
459        if CURRENT_DEVICE.has_natural_light() {
460            let max_label_width = {
461                let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
462                ["Intensity", "Warmth"]
463                    .iter()
464                    .map(|t| font.plan(t, None, None).width)
465                    .max()
466                    .unwrap() as i32
467            };
468            for i in 0..2usize {
469                let min_y = rect.min.y + (i + 1) as i32 * small_height;
470                self.children[index].resize(
471                    rect![
472                        rect.min.x + padding,
473                        min_y,
474                        rect.min.x + 2 * padding + max_label_width,
475                        min_y + small_height
476                    ],
477                    hub,
478                    rq,
479                    context,
480                );
481                self.children[index + 1].resize(
482                    rect![
483                        rect.min.x + max_label_width + 3 * padding,
484                        min_y,
485                        rect.max.x - padding,
486                        min_y + small_height
487                    ],
488                    hub,
489                    rq,
490                    context,
491                );
492                index += 2;
493            }
494            button_y += small_height;
495        } else {
496            let min_y = rect.min.y + small_height;
497            self.children[2].resize(
498                rect![
499                    rect.min.x + padding,
500                    min_y,
501                    rect.max.x - padding,
502                    min_y + small_height
503                ],
504                hub,
505                rq,
506                context,
507            );
508            index += 1;
509        }
510
511        let max_label_width = {
512            let font = font_from_style(&mut context.fonts, &NORMAL_STYLE, dpi);
513            [LABEL_SAVE, LABEL_GUESS]
514                .iter()
515                .map(|t| font.plan(t, None, None).width)
516                .max()
517                .unwrap() as i32
518        };
519
520        let button_height = 4 * x_height;
521
522        self.children[index].resize(
523            rect![
524                rect.min.x + 3 * padding,
525                button_y + small_height - button_height,
526                rect.min.x + 5 * padding + max_label_width,
527                button_y + small_height
528            ],
529            hub,
530            rq,
531            context,
532        );
533        index += 1;
534
535        self.children[index].resize(
536            rect![
537                rect.max.x - 5 * padding - max_label_width,
538                button_y + small_height - button_height,
539                rect.max.x - 3 * padding,
540                button_y + small_height
541            ],
542            hub,
543            rq,
544            context,
545        );
546        index += 1;
547
548        if !context.settings.frontlight_presets.is_empty() {
549            let presets_rect = rect![
550                rect.min.x + thickness + 4 * padding,
551                rect.max.y - small_height - 2 * padding,
552                rect.max.x - thickness - 4 * padding,
553                rect.max.y - thickness - 2 * padding
554            ];
555            self.children[index].resize(presets_rect, hub, rq, context);
556        }
557    }
558
559    fn is_background(&self) -> bool {
560        true
561    }
562
563    fn rect(&self) -> &Rectangle {
564        &self.rect
565    }
566
567    fn rect_mut(&mut self) -> &mut Rectangle {
568        &mut self.rect
569    }
570
571    fn children(&self) -> &Vec<Box<dyn View>> {
572        &self.children
573    }
574
575    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
576        &mut self.children
577    }
578
579    fn id(&self) -> Id {
580        self.id
581    }
582}