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