dais_ui/presenter/
overview.rs1use dais_core::bus::CommandSender;
6use dais_core::commands::Command;
7use dais_core::state::PresentationState;
8use dais_document::cache::PageCache;
9use dais_document::render_pipeline::FALLBACK_RENDER_SIZE;
10
11use crate::widgets::SlideThumbnail;
12
13pub struct OverviewGrid {
15 thumbnails: Vec<SlideThumbnail>,
16 selected: usize,
17 columns: usize,
18}
19
20const THUMB_WIDTH: f32 = 200.0;
22const THUMB_HEIGHT: f32 = 150.0;
23const THUMB_PADDING: f32 = 8.0;
24
25impl OverviewGrid {
26 pub fn new() -> Self {
27 Self { thumbnails: Vec::new(), selected: 0, columns: 4 }
28 }
29
30 pub fn show(
32 &mut self,
33 ctx: &egui::Context,
34 ui: &mut egui::Ui,
35 state: &PresentationState,
36 cache: &mut PageCache,
37 sender: &CommandSender,
38 ) {
39 if !state.overview_visible {
40 self.selected = state.current_logical_slide;
42 return;
43 }
44
45 while self.thumbnails.len() < state.total_logical_slides {
46 self.thumbnails.push(SlideThumbnail::new());
47 }
48
49 let available = ui.available_rect_before_wrap();
50 ui.painter().rect_filled(
51 available,
52 0.0,
53 egui::Color32::from_rgba_unmultiplied(0, 0, 0, 220),
54 );
55
56 #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
57 {
58 self.columns =
59 ((available.width() / (THUMB_WIDTH + THUMB_PADDING * 2.0)) as usize).max(1);
60 }
61
62 self.handle_navigation(ctx, state, sender);
63 self.render_grid(ctx, ui, state, cache, sender);
64 }
65
66 fn handle_navigation(
67 &mut self,
68 ctx: &egui::Context,
69 state: &PresentationState,
70 sender: &CommandSender,
71 ) {
72 let navigate_cmd = ctx.input(|i| {
73 if i.key_pressed(egui::Key::ArrowRight) {
74 Some(NavigateDir::Right)
75 } else if i.key_pressed(egui::Key::ArrowLeft) {
76 Some(NavigateDir::Left)
77 } else if i.key_pressed(egui::Key::ArrowDown) {
78 Some(NavigateDir::Down)
79 } else if i.key_pressed(egui::Key::ArrowUp) {
80 Some(NavigateDir::Up)
81 } else if i.key_pressed(egui::Key::Enter) {
82 Some(NavigateDir::Select)
83 } else if i.key_pressed(egui::Key::Escape) {
84 Some(NavigateDir::Close)
85 } else {
86 None
87 }
88 });
89
90 if let Some(dir) = navigate_cmd {
91 match dir {
92 NavigateDir::Right if self.selected + 1 < state.total_logical_slides => {
93 self.selected += 1;
94 }
95 NavigateDir::Left if self.selected > 0 => {
96 self.selected -= 1;
97 }
98 NavigateDir::Down if self.selected + self.columns < state.total_logical_slides => {
99 self.selected += self.columns;
100 }
101 NavigateDir::Up if self.selected >= self.columns => {
102 self.selected -= self.columns;
103 }
104 NavigateDir::Select => {
105 let _ = sender.send(Command::GoToSlide(self.selected));
106 let _ = sender.send(Command::ToggleSlideOverview);
107 }
108 NavigateDir::Close => {
109 let _ = sender.send(Command::ToggleSlideOverview);
110 }
111 _ => {}
112 }
113 }
114 }
115
116 fn render_grid(
117 &mut self,
118 ctx: &egui::Context,
119 ui: &mut egui::Ui,
120 state: &PresentationState,
121 cache: &mut PageCache,
122 sender: &CommandSender,
123 ) {
124 let render_size = FALLBACK_RENDER_SIZE;
125
126 egui::ScrollArea::vertical().show(ui, |ui| {
127 ui.horizontal_wrapped(|ui| {
128 ui.spacing_mut().item_spacing = egui::vec2(THUMB_PADDING, THUMB_PADDING);
129
130 for i in 0..state.total_logical_slides {
131 self.render_thumbnail(ctx, ui, i, state, cache, sender, render_size);
132 }
133 });
134 });
135 }
136
137 #[allow(clippy::too_many_arguments)]
138 fn render_thumbnail(
139 &mut self,
140 ctx: &egui::Context,
141 ui: &mut egui::Ui,
142 index: usize,
143 state: &PresentationState,
144 cache: &mut PageCache,
145 sender: &CommandSender,
146 render_size: dais_document::page::RenderSize,
147 ) {
148 let first_page =
149 state.slide_groups.get(index).and_then(|g| g.pages.first().copied()).unwrap_or(index);
150
151 if let Some(page) = cache.get(first_page, render_size) {
153 self.thumbnails[index].update(ctx, page, first_page);
154 }
155
156 let desired = egui::vec2(THUMB_WIDTH, THUMB_HEIGHT + 20.0);
157 let (rect, response) = ui.allocate_exact_size(desired, egui::Sense::click());
158
159 let thumb_rect = egui::Rect::from_min_size(rect.min, egui::vec2(THUMB_WIDTH, THUMB_HEIGHT));
160 let mut thumb_ui = ui.new_child(egui::UiBuilder::new().max_rect(thumb_rect));
161 self.thumbnails[index].show(&mut thumb_ui, egui::vec2(THUMB_WIDTH, THUMB_HEIGHT));
162
163 if index == self.selected {
164 ui.painter().rect_stroke(
165 thumb_rect,
166 2.0,
167 egui::Stroke::new(3.0, egui::Color32::LIGHT_BLUE),
168 egui::StrokeKind::Outside,
169 );
170 }
171
172 let label_rect = egui::Rect::from_min_size(
173 rect.min + egui::vec2(0.0, THUMB_HEIGHT),
174 egui::vec2(THUMB_WIDTH, 20.0),
175 );
176 ui.painter().text(
177 label_rect.center(),
178 egui::Align2::CENTER_CENTER,
179 format!("{}", index + 1),
180 egui::FontId::proportional(12.0),
181 egui::Color32::LIGHT_GRAY,
182 );
183
184 if response.clicked() {
185 let _ = sender.send(Command::GoToSlide(index));
186 let _ = sender.send(Command::ToggleSlideOverview);
187 }
188 }
189}
190
191impl Default for OverviewGrid {
192 fn default() -> Self {
193 Self::new()
194 }
195}
196
197#[derive(Debug, Clone, Copy)]
198enum NavigateDir {
199 Left,
200 Right,
201 Up,
202 Down,
203 Select,
204 Close,
205}