cadmus_core/view/
battery.rs

1use super::icon::ICONS_PIXMAPS;
2use super::{Bus, Event, Hub, Id, RenderData, RenderQueue, View, ViewId, ID_FEEDER};
3use super::{BORDER_RADIUS_SMALL, THICKNESS_LARGE, THICKNESS_MEDIUM};
4use crate::battery::Status;
5use crate::color::{BATTERY_FILL, BLACK, WHITE};
6use crate::context::Context;
7use crate::device::CURRENT_DEVICE;
8use crate::font::Fonts;
9use crate::framebuffer::{Framebuffer, UpdateMode};
10use crate::geom::{BorderSpec, CornerSpec, Rectangle};
11use crate::gesture::GestureEvent;
12use crate::unit::scale_by_dpi;
13
14const BUMP_HEIGHT: f32 = 5.0 * THICKNESS_LARGE;
15const BUMP_WIDTH: f32 = 4.0 * THICKNESS_LARGE;
16const BATTERY_HEIGHT: f32 = 11.0 * THICKNESS_LARGE;
17const BATTERY_WIDTH: f32 = 2.0 * BATTERY_HEIGHT;
18
19pub struct Battery {
20    id: Id,
21    rect: Rectangle,
22    children: Vec<Box<dyn View>>,
23    status: Status,
24    capacity: f32,
25}
26
27impl Battery {
28    pub fn new(rect: Rectangle, capacity: f32, status: Status) -> Battery {
29        Battery {
30            id: ID_FEEDER.next(),
31            rect,
32            children: Vec::new(),
33            capacity,
34            status,
35        }
36    }
37
38    pub fn update(&mut self, rq: &mut RenderQueue, context: &mut Context) {
39        self.capacity = context.battery.capacity().map_or(self.capacity, |v| v[0]);
40        self.status = context.battery.status().map_or(self.status, |v| v[0]);
41        rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
42    }
43}
44
45impl View for Battery {
46    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
47    fn handle_event(
48        &mut self,
49        evt: &Event,
50        _hub: &Hub,
51        bus: &mut Bus,
52        rq: &mut RenderQueue,
53        context: &mut Context,
54    ) -> bool {
55        match *evt {
56            Event::BatteryTick => {
57                self.update(rq, context);
58                true
59            }
60            Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => {
61                bus.push_back(Event::ToggleNear(ViewId::BatteryMenu, self.rect));
62                true
63            }
64            _ => false,
65        }
66    }
67
68    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, _fonts, _rect), fields(rect = ?_rect)))]
69    fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {
70        let dpi = CURRENT_DEVICE.dpi;
71
72        let border_radius = scale_by_dpi(BORDER_RADIUS_SMALL, dpi) as i32;
73        let border_thickness = scale_by_dpi(THICKNESS_LARGE, dpi) as i32;
74
75        let batt_width = scale_by_dpi(BATTERY_WIDTH, dpi) as i32;
76        let batt_height = scale_by_dpi(BATTERY_HEIGHT, dpi) as i32;
77
78        let bump_width = scale_by_dpi(BUMP_WIDTH, dpi) as i32;
79        let bump_height = scale_by_dpi(BUMP_HEIGHT, dpi) as i32;
80
81        let edge_width = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
82
83        let dx = (self.rect.width() as i32 - (batt_width + bump_width - border_thickness)) / 2;
84        let dy = (self.rect.height() as i32 - batt_height) / 2;
85
86        let mut pt = self.rect.min + pt!(dx, dy);
87        let batt_rect = rect![pt, pt + pt!(batt_width, batt_height)];
88
89        fb.draw_rectangle(&self.rect, WHITE);
90
91        let max_fill_width = batt_width - 2 * border_thickness;
92        let fill_width = (self.capacity.clamp(0.0, 100.0) / 100.0 * max_fill_width as f32) as i32;
93        let fill_height = batt_height - 2 * border_thickness;
94        let x_offset_edge = pt.x + border_thickness + fill_width;
95        let x_offset_fill = x_offset_edge.saturating_sub(edge_width);
96
97        fb.draw_rounded_rectangle_with_border(
98            &batt_rect,
99            &CornerSpec::Uniform(border_radius),
100            &BorderSpec {
101                thickness: border_thickness as u16,
102                color: BLACK,
103            },
104            &|x, _| {
105                if x <= x_offset_fill {
106                    BATTERY_FILL
107                } else if x <= x_offset_edge {
108                    BLACK
109                } else {
110                    WHITE
111                }
112            },
113        );
114
115        pt += pt!(
116            batt_width - border_thickness as i32,
117            (batt_height - bump_height) / 2
118        );
119        let bump_rect = rect![pt, pt + pt!(bump_width, bump_height)];
120
121        fb.draw_rounded_rectangle_with_border(
122            &bump_rect,
123            &CornerSpec::East(border_radius / 2),
124            &BorderSpec {
125                thickness: border_thickness as u16,
126                color: BLACK,
127            },
128            &WHITE,
129        );
130
131        pt = self.rect.min + pt!(dx, dy) + pt!(border_thickness);
132
133        if self.status.is_wired() {
134            let name = if self.status == Status::Charging {
135                "plug"
136            } else {
137                "check_mark-small"
138            };
139            let pixmap = ICONS_PIXMAPS.get(name).unwrap();
140            pt += pt!(
141                (max_fill_width - pixmap.width as i32) / 2,
142                (fill_height - pixmap.height as i32) / 2
143            );
144            fb.draw_blended_pixmap(pixmap, pt, BLACK);
145        }
146    }
147
148    fn rect(&self) -> &Rectangle {
149        &self.rect
150    }
151
152    fn rect_mut(&mut self) -> &mut Rectangle {
153        &mut self.rect
154    }
155
156    fn children(&self) -> &Vec<Box<dyn View>> {
157        &self.children
158    }
159
160    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
161        &mut self.children
162    }
163
164    fn id(&self) -> Id {
165        self.id
166    }
167}