1use super::{Align, Bus, Event, Hub, Id, RenderData, RenderQueue, View, ViewId, ID_FEEDER};
2use crate::color::{Color, TEXT_INVERTED_HARD, TEXT_NORMAL};
3use crate::context::Context;
4use crate::device::CURRENT_DEVICE;
5use crate::document::pdf::PdfOpener;
6use crate::font::Fonts;
7use crate::framebuffer::{Framebuffer, Pixmap, UpdateMode};
8use crate::geom::{CornerSpec, Rectangle};
9use crate::gesture::GestureEvent;
10use crate::input::{DeviceEvent, FingerStatus};
11use crate::unit::scale_by_dpi_raw;
12use fxhash::FxHashMap;
13use lazy_static::lazy_static;
14use std::path::Path;
15
16const ICON_SCALE: f32 = 1.0 / 32.0;
17
18lazy_static! {
19 pub static ref ICONS_PIXMAPS: FxHashMap<&'static str, Pixmap> = {
20 let mut m = FxHashMap::default();
21 let scale = scale_by_dpi_raw(ICON_SCALE, CURRENT_DEVICE.dpi);
22 #[cfg(test)]
23 let dir = Path::new(
24 &std::env::var("TEST_ROOT_DIR").expect("TEST_ROOT_DIR must be set for tests."),
25 )
26 .join("icons");
27 #[cfg(not(test))]
28 let dir = Path::new("icons").to_path_buf();
29 for name in [
30 "home",
31 "search",
32 "back",
33 "frontlight",
34 "frontlight-disabled",
35 "menu",
36 "angle-left",
37 "angle-right",
38 "angle-left-small",
39 "angle-right-small",
40 "return",
41 "shift",
42 "combine",
43 "alternate",
44 "delete-backward",
45 "delete-forward",
46 "move-backward",
47 "move-backward-short",
48 "move-forward",
49 "move-forward-short",
50 "close",
51 "check_mark-small",
52 "check_mark",
53 "check_mark-large",
54 "bullet",
55 "arrow-left",
56 "arrow-right",
57 "angle-down",
58 "angle-up",
59 "crop",
60 "toc",
61 "font_family",
62 "font_size",
63 "line_height",
64 "align-justify",
65 "align-left",
66 "align-right",
67 "align-center",
68 "margin",
69 "plug",
70 "cover",
71 "enclosed_menu",
72 "contrast",
73 "gray",
74 "plus",
75 ]
76 .iter()
77 .cloned()
78 {
79 let path = dir.join(format!("{}.svg", name));
80 let doc = PdfOpener::new().and_then(|o| o.open(&path).ok()).unwrap();
81 let pixmap = doc.page(0).and_then(|p| p.pixmap(scale, 1)).unwrap();
82 m.insert(name, pixmap);
83 }
84 m
85 };
86}
87
88pub struct Icon {
89 id: Id,
90 pub rect: Rectangle,
91 children: Vec<Box<dyn View>>,
92 pub name: String,
93 background: Color,
94 align: Align,
95 corners: Option<CornerSpec>,
96 event: Event,
97 pub active: bool,
98}
99
100impl Icon {
101 pub fn new(name: &str, rect: Rectangle, event: Event) -> Icon {
102 Icon {
103 id: ID_FEEDER.next(),
104 rect,
105 children: Vec::new(),
106 name: name.to_string(),
107 background: TEXT_NORMAL[0],
108 align: Align::Center,
109 corners: None,
110 event,
111 active: false,
112 }
113 }
114
115 pub fn background(mut self, background: Color) -> Icon {
116 self.background = background;
117 self
118 }
119
120 pub fn align(mut self, align: Align) -> Icon {
121 self.align = align;
122 self
123 }
124
125 pub fn corners(mut self, corners: Option<CornerSpec>) -> Icon {
126 self.corners = corners;
127 self
128 }
129}
130
131impl View for Icon {
132 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, hub, bus, rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
133 fn handle_event(
134 &mut self,
135 evt: &Event,
136 hub: &Hub,
137 bus: &mut Bus,
138 rq: &mut RenderQueue,
139 _context: &mut Context,
140 ) -> bool {
141 match *evt {
142 Event::Device(DeviceEvent::Finger {
143 status, position, ..
144 }) => match status {
145 FingerStatus::Down if self.rect.includes(position) => {
146 self.active = true;
147 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Fast));
148 true
149 }
150 FingerStatus::Up if self.active => {
151 self.active = false;
152 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
153 true
154 }
155 _ => false,
156 },
157 Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => {
158 bus.push_back(self.event.clone());
159 true
160 }
161 Event::Gesture(GestureEvent::HoldFingerShort(center, ..))
162 if self.rect.includes(center) =>
163 {
164 match self.event {
165 Event::Page(dir) => bus.push_back(Event::Chapter(dir)),
166 Event::Show(ViewId::Frontlight) => {
167 hub.send(Event::ToggleFrontlight).ok();
168 }
169 Event::Show(ViewId::MarginCropper) => {
170 bus.push_back(Event::ToggleNear(ViewId::MarginCropperMenu, self.rect));
171 }
172 Event::History(dir, false) => {
173 bus.push_back(Event::History(dir, true));
174 }
175 _ => (),
176 }
177 true
178 }
179 _ => false,
180 }
181 }
182
183 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, _fonts, _rect), fields(rect = ?_rect)))]
184 fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {
185 let scheme = if self.active {
186 TEXT_INVERTED_HARD
187 } else {
188 TEXT_NORMAL
189 };
190
191 let pixmap = ICONS_PIXMAPS.get(&self.name[..]).unwrap();
192 let dx = self
193 .align
194 .offset(pixmap.width as i32, self.rect.width() as i32);
195 let dy = (self.rect.height() as i32 - pixmap.height as i32) / 2;
196 let pt = self.rect.min + pt!(dx, dy);
197
198 let background = if self.active {
199 scheme[0]
200 } else {
201 self.background
202 };
203
204 if let Some(ref cs) = self.corners {
205 fb.draw_rounded_rectangle(&self.rect, cs, background);
206 } else {
207 fb.draw_rectangle(&self.rect, background);
208 }
209
210 fb.draw_blended_pixmap(pixmap, pt, scheme[1]);
211 }
212
213 fn resize(
214 &mut self,
215 rect: Rectangle,
216 _hub: &Hub,
217 _rq: &mut RenderQueue,
218 _context: &mut Context,
219 ) {
220 if let Event::ToggleNear(_, ref mut event_rect) = self.event {
221 *event_rect = rect;
222 }
223 self.rect = rect;
224 }
225
226 fn rect(&self) -> &Rectangle {
227 &self.rect
228 }
229
230 fn rect_mut(&mut self) -> &mut Rectangle {
231 &mut self.rect
232 }
233
234 fn children(&self) -> &Vec<Box<dyn View>> {
235 &self.children
236 }
237
238 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
239 &mut self.children
240 }
241
242 fn id(&self) -> Id {
243 self.id
244 }
245}