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::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        // Preview
144        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        // Author
165        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        // Title
174        {
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        // Year or Progress
214        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        // File kind
283        {
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        // File size
297        {
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}