1use crate::color::{BLACK, READING_PROGRESS, WHITE};
2use crate::color::{TEXT_INVERTED_HARD, TEXT_NORMAL};
3use crate::context::Context;
4use crate::device::CURRENT_DEVICE;
5use crate::document::pdf::PdfOpener;
6use crate::document::{Document, HumanSize, Location};
7use crate::font::{font_from_style, Fonts};
8use crate::font::{MD_AUTHOR, MD_KIND, MD_SIZE, MD_TITLE, MD_YEAR};
9use crate::framebuffer::{Framebuffer, UpdateMode};
10use crate::geom::{halves, BorderSpec, CornerSpec, Rectangle};
11use crate::gesture::GestureEvent;
12use crate::metadata::{Info, Status};
13use crate::settings::{FirstColumn, SecondColumn};
14use crate::unit::scale_by_dpi;
15use crate::view::{Bus, Event, Hub, Id, RenderData, RenderQueue, View, ID_FEEDER, THICKNESS_SMALL};
16use std::path::PathBuf;
17
18const PROGRESS_HEIGHT: f32 = 13.0;
19
20pub struct Book {
21 id: Id,
22 rect: Rectangle,
23 children: Vec<Box<dyn View>>,
24 info: Info,
25 index: usize,
26 first_column: FirstColumn,
27 second_column: SecondColumn,
28 preview_path: Option<PathBuf>,
29 active: bool,
30}
31
32impl Book {
33 pub fn new(
34 rect: Rectangle,
35 info: Info,
36 index: usize,
37 first_column: FirstColumn,
38 second_column: SecondColumn,
39 preview_path: Option<PathBuf>,
40 ) -> Book {
41 Book {
42 id: ID_FEEDER.next(),
43 rect,
44 children: Vec::new(),
45 info,
46 index,
47 first_column,
48 second_column,
49 preview_path,
50 active: false,
51 }
52 }
53}
54
55impl View for Book {
56 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, hub, bus, rq, _context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
57 fn handle_event(
58 &mut self,
59 evt: &Event,
60 hub: &Hub,
61 bus: &mut Bus,
62 rq: &mut RenderQueue,
63 _context: &mut Context,
64 ) -> bool {
65 match *evt {
66 Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => {
67 self.active = true;
68 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
69 hub.send(Event::Open(Box::new(self.info.clone()))).ok();
70 true
71 }
72 Event::Gesture(GestureEvent::HoldFingerShort(center, ..))
73 if self.rect.includes(center) =>
74 {
75 let pt = pt!(center.x, self.rect.center().y);
76 bus.push_back(Event::ToggleBookMenu(Rectangle::from_point(pt), self.index));
77 true
78 }
79 Event::RefreshBookPreview(ref path, ref preview_path) => {
80 if self.info.file.path == *path {
81 self.preview_path = preview_path.clone();
82 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
83 true
84 } else {
85 false
86 }
87 }
88 Event::Invalid(ref path) => {
89 if self.info.file.path == *path {
90 self.active = false;
91 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
92 true
93 } else {
94 false
95 }
96 }
97 _ => false,
98 }
99 }
100 #[cfg_attr(feature = "otel", tracing::instrument(skip(self, fb, fonts, _rect), fields(rect = ?_rect)))]
101 fn render(&self, fb: &mut dyn Framebuffer, _rect: Rectangle, fonts: &mut Fonts) {
102 let dpi = CURRENT_DEVICE.dpi;
103
104 let scheme = if self.active {
105 TEXT_INVERTED_HARD
106 } else {
107 TEXT_NORMAL
108 };
109
110 fb.draw_rectangle(&self.rect, scheme[0]);
111
112 let (title, author) = if self.first_column == FirstColumn::TitleAndAuthor {
113 (self.info.title(), self.info.author.as_str())
114 } else {
115 let filename = self
116 .info
117 .file
118 .path
119 .file_stem()
120 .map(|v| v.to_string_lossy().into_owned())
121 .unwrap_or_default();
122 (filename, "")
123 };
124
125 let year = &self.info.year;
126 let file_info = &self.info.file;
127
128 let (x_height, padding, baseline) = {
129 let font = font_from_style(fonts, &MD_TITLE, dpi);
130 let x_height = font.x_heights.0 as i32;
131 (
132 x_height,
133 font.em() as i32,
134 (self.rect.height() as i32 - 2 * x_height) / 3,
135 )
136 };
137
138 let (small_half_padding, big_half_padding) = halves(padding);
139 let third_width = 6 * x_height;
140 let second_width = 8 * x_height;
141 let first_width = self.rect.width() as i32 - second_width - third_width;
142 let mut width = first_width - padding - small_half_padding;
143 let mut start_x = self.rect.min.x + padding;
144
145 if let Some(preview_path) = self.preview_path.as_ref() {
147 let th = self.rect.height() as i32 - x_height;
148 let tw = 3 * th / 4;
149
150 if preview_path.exists() {
151 if let Some((pixmap, _)) = PdfOpener::new()
152 .and_then(|opener| opener.open(preview_path))
153 .and_then(|mut doc| {
154 doc.dims(0).and_then(|dims| {
155 let scale = (tw as f32 / dims.0).min(th as f32 / dims.1);
156 doc.pixmap(Location::Exact(0), scale, CURRENT_DEVICE.color_samples())
157 })
158 })
159 {
160 let dx = (tw - pixmap.width as i32) / 2;
161 let dy = (th - pixmap.height as i32) / 2;
162 let pt = pt!(
163 self.rect.min.x + padding + dx,
164 self.rect.min.y + x_height / 2 + dy
165 );
166 fb.draw_pixmap(&pixmap, pt);
167 if fb.inverted() {
168 let rect = pixmap.rect() + pt;
169 fb.invert_region(&rect);
170 }
171 }
172 }
173
174 width -= tw + padding;
175 start_x += tw + padding;
176 }
177
178 let author_width = {
180 let font = font_from_style(fonts, &MD_AUTHOR, dpi);
181 let plan = font.plan(author, Some(width), None);
182 let pt = pt!(start_x, self.rect.max.y - baseline);
183 font.render(fb, scheme[1], &plan, pt);
184 plan.width
185 };
186
187 {
189 let font = font_from_style(fonts, &MD_TITLE, dpi);
190 let mut plan = font.plan(&title, None, None);
191 let mut title_lines = 1;
192
193 if plan.width > width {
194 let available = width - author_width;
195 if available > 3 * padding {
196 let (index, usable_width) = font.cut_point(&plan, width);
197 let leftover = plan.width - usable_width;
198 if leftover > 2 * padding {
199 let mut plan2 = plan.split_off(index, usable_width);
200 let max_width = available - if author_width > 0 { padding } else { 0 };
201 font.trim_left(&mut plan2);
202 font.crop_right(&mut plan2, max_width);
203 let pt = pt!(
204 self.rect.min.x + first_width - small_half_padding - plan2.width,
205 self.rect.max.y - baseline
206 );
207 font.render(fb, scheme[1], &plan2, pt);
208 title_lines += 1;
209 } else {
210 font.crop_right(&mut plan, width);
211 }
212 } else {
213 font.crop_right(&mut plan, width);
214 }
215 }
216
217 let dy = if author_width == 0 && title_lines == 1 {
218 (self.rect.height() as i32 - x_height) / 2 + x_height
219 } else {
220 baseline + x_height
221 };
222
223 let pt = pt!(start_x, self.rect.min.y + dy);
224 font.render(fb, scheme[1], &plan, pt);
225 }
226
227 match self.second_column {
229 SecondColumn::Year => {
230 let font = font_from_style(fonts, &MD_YEAR, dpi);
231 let plan = font.plan(year, None, None);
232 let dx = (second_width - padding - plan.width) / 2;
233 let dy = (self.rect.height() as i32 - font.x_heights.1 as i32) / 2;
234 let pt = pt!(
235 self.rect.min.x + first_width + big_half_padding + dx,
236 self.rect.max.y - dy
237 );
238 font.render(fb, scheme[1], &plan, pt);
239 }
240 SecondColumn::Progress => {
241 let progress_height = scale_by_dpi(PROGRESS_HEIGHT, dpi) as i32;
242 let thickness = scale_by_dpi(THICKNESS_SMALL, dpi) as u16;
243 let (small_radius, big_radius) = halves(progress_height);
244 let center = pt!(
245 self.rect.min.x + first_width + second_width / 2,
246 self.rect.min.y + self.rect.height() as i32 / 2
247 );
248 match self.info.status() {
249 Status::New | Status::Finished => {
250 let color = if self.info.reader.is_none() {
251 WHITE
252 } else {
253 BLACK
254 };
255 fb.draw_rounded_rectangle_with_border(
256 &rect![
257 center - pt!(small_radius, small_radius),
258 center + pt!(big_radius, big_radius)
259 ],
260 &CornerSpec::Uniform(small_radius),
261 &BorderSpec {
262 thickness,
263 color: BLACK,
264 },
265 &color,
266 );
267 }
268 Status::Reading(progress) => {
269 let progress_width = 2 * (second_width - padding) / 3;
270 let (small_progress_width, big_progress_width) = halves(progress_width);
271 let x_offset = center.x - progress_width / 2
272 + (progress_width as f32 * progress.min(1.0)) as i32;
273 fb.draw_rounded_rectangle_with_border(
274 &rect![
275 center - pt!(small_progress_width, small_radius),
276 center + pt!(big_progress_width, big_radius)
277 ],
278 &CornerSpec::Uniform(small_radius),
279 &BorderSpec {
280 thickness,
281 color: BLACK,
282 },
283 &|x, _| {
284 if x < x_offset {
285 READING_PROGRESS
286 } else {
287 WHITE
288 }
289 },
290 );
291 }
292 }
293 }
294 }
295
296 {
298 let kind = file_info.kind.to_uppercase();
299 let font = font_from_style(fonts, &MD_KIND, dpi);
300 let mut plan = font.plan(&kind, None, None);
301 let letter_spacing = scale_by_dpi(3.0, dpi) as i32;
302 plan.space_out(letter_spacing);
303 let pt = pt!(
304 self.rect.max.x - padding - plan.width,
305 self.rect.min.y + baseline + x_height
306 );
307 font.render(fb, scheme[1], &plan, pt);
308 }
309
310 {
312 let size = file_info.size.human_size();
313 let font = font_from_style(fonts, &MD_SIZE, dpi);
314 let plan = font.plan(&size, None, None);
315 let pt = pt!(
316 self.rect.max.x - padding - plan.width,
317 self.rect.max.y - baseline
318 );
319 font.render(fb, scheme[1], &plan, pt);
320 }
321 }
322
323 fn rect(&self) -> &Rectangle {
324 &self.rect
325 }
326
327 fn rect_mut(&mut self) -> &mut Rectangle {
328 &mut self.rect
329 }
330
331 fn children(&self) -> &Vec<Box<dyn View>> {
332 &self.children
333 }
334
335 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
336 &mut self.children
337 }
338
339 fn id(&self) -> Id {
340 self.id
341 }
342}