cadmus_core/view/home/
book.rs

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        // Preview
146        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        // Author
179        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        // Title
188        {
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        // Year or Progress
228        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        // File kind
297        {
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        // File size
311        {
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}