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}