1use std::collections::HashSet;
2
3use crate::search::{compare_heading, find_and_mark};
4
5use super::{
6 image::ImageComponent,
7 textcomponent::{TextComponent, TextNode},
8 word::{Word, WordType},
9};
10
11pub struct ComponentRoot {
12 file_name: Option<String>,
13 components: Vec<Component>,
14 is_focused: bool,
15}
16
17impl ComponentRoot {
18 #[must_use]
19 pub fn new(file_name: Option<String>, components: Vec<Component>) -> Self {
20 Self {
21 file_name,
22 components,
23 is_focused: false,
24 }
25 }
26
27 #[must_use]
28 pub fn children(&self) -> Vec<&Component> {
29 self.components.iter().collect()
30 }
31
32 pub fn children_mut(&mut self) -> Vec<&mut Component> {
33 self.components.iter_mut().collect()
34 }
35
36 #[must_use]
37 pub fn components(&self) -> Vec<&TextComponent> {
38 self.components
39 .iter()
40 .filter_map(|c| match c {
41 Component::TextComponent(comp) => Some(comp),
42 Component::Image(_) => None,
43 })
44 .collect()
45 }
46
47 pub fn components_mut(&mut self) -> Vec<&mut TextComponent> {
48 self.components
49 .iter_mut()
50 .filter_map(|c| match c {
51 Component::TextComponent(comp) => Some(comp),
52 Component::Image(_) => None,
53 })
54 .collect()
55 }
56
57 #[must_use]
58 pub fn file_name(&self) -> Option<&str> {
59 self.file_name.as_deref()
60 }
61
62 #[must_use]
63 pub fn words(&self) -> Vec<&Word> {
64 self.components
65 .iter()
66 .filter_map(|c| match c {
67 Component::TextComponent(comp) => Some(comp),
68 Component::Image(_) => None,
69 })
70 .flat_map(|c| c.content().iter().flatten())
71 .collect()
72 }
73
74 pub fn find_and_mark(&mut self, search: &str) {
75 let mut words = self
76 .components
77 .iter_mut()
78 .filter_map(|c| match c {
79 Component::TextComponent(comp) => Some(comp),
80 Component::Image(_) => None,
81 })
82 .flat_map(|c| c.words_mut())
83 .collect::<Vec<_>>();
84 find_and_mark(search, &mut words);
85 }
86
87 #[must_use]
88 pub fn search_results_heights(&self) -> Vec<usize> {
89 self.components
90 .iter()
91 .filter_map(|c| match c {
92 Component::TextComponent(comp) => Some(comp),
93 Component::Image(_) => None,
94 })
95 .flat_map(|c| {
96 let mut heights = c.selected_heights();
97 heights.iter_mut().for_each(|h| *h += c.y_offset() as usize);
98 heights
99 })
100 .collect()
101 }
102
103 pub fn clear(&mut self) {
104 self.file_name = None;
105 self.components.clear();
106 }
107
108 pub fn select(&mut self, index: usize) -> Result<u16, String> {
109 self.deselect();
110 self.is_focused = true;
111 let mut count = 0;
112 for comp in self.components.iter_mut().filter_map(|f| match f {
113 Component::TextComponent(comp) => Some(comp),
114 Component::Image(_) => None,
115 }) {
116 let link_inside_comp = index - count < comp.num_links();
117 if link_inside_comp {
118 comp.visually_select(index - count)?;
119 return Ok(comp.y_offset());
120 }
121 count += comp.num_links();
122 }
123 Err(format!("Index out of bounds: {index} >= {count}"))
124 }
125
126 pub fn deselect(&mut self) {
127 self.is_focused = false;
128 for comp in self.components.iter_mut().filter_map(|f| match f {
129 Component::TextComponent(comp) => Some(comp),
130 Component::Image(_) => None,
131 }) {
132 comp.deselect();
133 }
134 }
135
136 #[must_use]
137 pub fn find_footnote(&self, search: &str) -> String {
138 let footnote = self
139 .components
140 .iter()
141 .filter_map(|f| match f {
142 Component::TextComponent(text_component) => {
143 if text_component.kind() == TextNode::Footnote {
144 Some(text_component)
145 } else {
146 None
147 }
148 }
149 Component::Image(_) => None,
150 })
151 .filter(|f| {
152 if let Some(foot_ref) = f.meta_info().iter().next() {
153 foot_ref.content() == search
154 } else {
155 false
156 }
157 })
158 .flat_map(|f| f.content().iter().flatten())
159 .filter(|f| f.kind() == WordType::Footnote)
160 .map(Word::content)
161 .collect::<String>();
162
163 if footnote.is_empty() {
164 String::from("Footnote not found")
165 } else {
166 footnote
167 }
168 }
169
170 #[must_use]
171 pub fn link_index_and_height(&self) -> Vec<(usize, u16)> {
172 let mut indexes = Vec::new();
173 let mut count = 0;
174 self.components
175 .iter()
176 .filter_map(|f| match f {
177 Component::TextComponent(comp) => Some(comp),
178 Component::Image(_) => None,
179 })
180 .filter(|comp| !comp.is_hidden())
181 .for_each(|comp| {
182 let height = comp.y_offset();
183 comp.content().iter().enumerate().for_each(|(index, row)| {
184 row.iter().for_each(|c| {
185 if matches!(
186 c.kind(),
187 WordType::Link | WordType::Selected | WordType::FootnoteInline
188 ) {
189 indexes.push((count, height + index as u16));
190 count += 1;
191 }
192 });
193 });
194 });
195
196 indexes
197 }
198
199 pub fn set_scroll(&mut self, scroll: u16) {
201 let mut y_offset = 0;
202 for component in &mut self.components {
203 component.set_y_offset(y_offset);
204 component.set_scroll_offset(scroll);
205 y_offset += component.height();
206 }
207 }
208
209 pub fn heading_offset(&self, heading: &str) -> Result<u16, String> {
210 let mut y_offset = 0;
211 for component in &self.components {
212 match component {
213 Component::TextComponent(comp) => {
214 if comp.kind() == TextNode::Heading
215 && compare_heading(&heading[1..], comp.content())
216 {
217 return Ok(y_offset);
218 }
219 y_offset += comp.height();
220 }
221 Component::Image(e) => y_offset += e.height(),
222 }
223 }
224 Err(format!("Heading not found: {heading}"))
225 }
226
227 #[must_use]
229 pub fn content(&self) -> Vec<String> {
230 self.components()
231 .iter()
232 .flat_map(|c| c.content_as_lines())
233 .collect()
234 }
235
236 #[must_use]
237 pub fn selected(&self) -> &str {
238 let block = self
239 .components
240 .iter()
241 .filter_map(|f| match f {
242 Component::TextComponent(comp) => Some(comp),
243 Component::Image(_) => None,
244 })
245 .find(|c| c.is_focused())
246 .unwrap();
247 block.highlight_link().unwrap()
248 }
249
250 #[must_use]
251 pub fn selected_underlying_type(&self) -> WordType {
252 let selected = self
253 .components
254 .iter()
255 .filter_map(|f| match f {
256 Component::TextComponent(comp) => Some(comp),
257 Component::Image(_) => None,
258 })
259 .find(|c| c.is_focused())
260 .unwrap()
261 .content()
262 .iter()
263 .flatten()
264 .filter(|c| c.kind() == WordType::Selected)
265 .collect::<Vec<_>>();
266
267 selected.first().unwrap().previous_type()
268 }
269
270 pub fn transform(&mut self, width: u16) {
272 for component in self.components_mut() {
273 component.transform(width);
274 }
275 }
276
277 #[must_use]
279 pub fn add_missing_components(self) -> Self {
280 let mut components = Vec::new();
281 let mut iter = self.components.into_iter().peekable();
282 while let Some(component) = iter.next() {
283 let kind = component.kind();
284 let curr_ids: Vec<u32> = match &component {
285 Component::TextComponent(tc) => tc.owning_details_ids().to_vec(),
286 Component::Image(_) => Vec::new(),
287 };
288 components.push(component);
289 if let Some(next) = iter.peek()
290 && kind != TextNode::LineBreak
291 && next.kind() != TextNode::LineBreak
292 {
293 let next_ids: Vec<u32> = match next {
294 Component::TextComponent(tc) => tc.owning_details_ids().to_vec(),
295 Component::Image(_) => Vec::new(),
296 };
297 let shared_ids: Vec<u32> = curr_ids
302 .iter()
303 .zip(next_ids.iter())
304 .take_while(|(a, b)| a == b)
305 .map(|(a, _)| *a)
306 .collect();
307 let mut lb = TextComponent::new(TextNode::LineBreak, Vec::new());
308 lb.set_owning_details_ids(shared_ids);
309 components.push(Component::TextComponent(lb));
310 }
311 }
312 Self {
313 file_name: self.file_name,
314 components,
315 is_focused: self.is_focused,
316 }
317 }
318
319 #[must_use]
320 pub fn height(&self) -> u16 {
321 self.components.iter().map(ComponentProps::height).sum()
322 }
323
324 #[must_use]
325 pub fn num_links(&self) -> usize {
326 self.components
327 .iter()
328 .filter_map(|f| match f {
329 Component::TextComponent(comp) => Some(comp),
330 Component::Image(_) => None,
331 })
332 .map(TextComponent::num_links)
333 .sum()
334 }
335
336 pub fn recompute_visibility(&mut self) {
342 let folded: HashSet<u32> = self
343 .components
344 .iter()
345 .filter_map(|f| match f {
346 Component::TextComponent(comp) => Some(comp),
347 Component::Image(_) => None,
348 })
349 .filter_map(|tc| match tc.kind() {
350 TextNode::DetailsSummary {
351 id, folded: true, ..
352 } => Some(id),
353 _ => None,
354 })
355 .collect();
356
357 for c in self.components.iter_mut() {
358 if let Component::TextComponent(tc) = c {
359 let hidden = tc.owning_details_ids().iter().any(|id| folded.contains(id));
360 tc.set_hidden(hidden);
361 }
362 }
363 }
364
365 #[must_use]
369 pub fn num_details(&self) -> usize {
370 self.components
371 .iter()
372 .filter_map(|f| match f {
373 Component::TextComponent(comp) => Some(comp),
374 Component::Image(_) => None,
375 })
376 .filter(|comp| {
377 !comp.is_hidden() && matches!(comp.kind(), TextNode::DetailsSummary { .. })
378 })
379 .count()
380 }
381
382 #[must_use]
386 pub fn details_index_and_height(&self) -> Vec<(usize, u16)> {
387 let mut out = Vec::new();
388 let mut idx = 0usize;
389 for c in &self.components {
390 if let Component::TextComponent(comp) = c
391 && !comp.is_hidden()
392 && matches!(comp.kind(), TextNode::DetailsSummary { .. })
393 {
394 out.push((idx, comp.y_offset()));
395 idx += 1;
396 }
397 }
398 out
399 }
400
401 pub fn select_details(&mut self, index: usize) -> Result<u16, String> {
405 self.deselect_details();
406 let mut count = 0;
407 for c in self.components.iter_mut() {
408 if let Component::TextComponent(comp) = c
409 && !comp.is_hidden()
410 && matches!(comp.kind(), TextNode::DetailsSummary { .. })
411 {
412 if count == index {
413 comp.visually_select_summary();
414 return Ok(comp.y_offset());
415 }
416 count += 1;
417 }
418 }
419 Err(format!("Details index out of bounds: {index} >= {count}"))
420 }
421
422 pub fn deselect_details(&mut self) {
424 for c in self.components.iter_mut() {
425 if let Component::TextComponent(comp) = c
426 && matches!(comp.kind(), TextNode::DetailsSummary { .. })
427 {
428 comp.deselect_summary();
429 }
430 }
431 }
432
433 pub fn toggle_selected_details(&mut self) -> Result<(), String> {
437 let mut toggled = false;
438 for c in self.components.iter_mut() {
439 if let Component::TextComponent(comp) = c
440 && comp.is_focused()
441 && let TextNode::DetailsSummary { folded, .. } = comp.kind()
442 {
443 comp.set_details_folded(!folded);
444 toggled = true;
445 break;
446 }
447 }
448 if !toggled {
449 return Err("No details summary is focused".to_string());
450 }
451 self.recompute_visibility();
452 Ok(())
453 }
454}
455
456pub trait ComponentProps {
457 fn height(&self) -> u16;
458 fn set_y_offset(&mut self, y_offset: u16);
459 fn set_scroll_offset(&mut self, scroll: u16);
460 fn kind(&self) -> TextNode;
461}
462
463pub enum Component {
464 TextComponent(TextComponent),
465 Image(ImageComponent),
466}
467
468impl From<TextComponent> for Component {
469 fn from(comp: TextComponent) -> Self {
470 Component::TextComponent(comp)
471 }
472}
473
474impl ComponentProps for Component {
475 fn height(&self) -> u16 {
476 match self {
477 Component::TextComponent(comp) => comp.height(),
478 Component::Image(comp) => comp.height(),
479 }
480 }
481
482 fn set_y_offset(&mut self, y_offset: u16) {
483 match self {
484 Component::TextComponent(comp) => comp.set_y_offset(y_offset),
485 Component::Image(comp) => comp.set_y_offset(y_offset),
486 }
487 }
488
489 fn set_scroll_offset(&mut self, scroll: u16) {
490 match self {
491 Component::TextComponent(comp) => comp.set_scroll_offset(scroll),
492 Component::Image(comp) => comp.set_scroll_offset(scroll),
493 }
494 }
495
496 fn kind(&self) -> TextNode {
497 match self {
498 Component::TextComponent(comp) => comp.kind(),
499 Component::Image(comp) => comp.kind(),
500 }
501 }
502}