1use crate::search::{compare_heading, find_and_mark};
2
3use super::{
4 image::ImageComponent,
5 textcomponent::{TextComponent, TextNode},
6 word::{Word, WordType},
7};
8
9pub struct ComponentRoot {
10 file_name: Option<String>,
11 components: Vec<Component>,
12 is_focused: bool,
13}
14
15impl ComponentRoot {
16 #[must_use]
17 pub fn new(file_name: Option<String>, components: Vec<Component>) -> Self {
18 Self {
19 file_name,
20 components,
21 is_focused: false,
22 }
23 }
24
25 #[must_use]
26 pub fn children(&self) -> Vec<&Component> {
27 self.components.iter().collect()
28 }
29
30 pub fn children_mut(&mut self) -> Vec<&mut Component> {
31 self.components.iter_mut().collect()
32 }
33
34 #[must_use]
35 pub fn components(&self) -> Vec<&TextComponent> {
36 self.components
37 .iter()
38 .filter_map(|c| match c {
39 Component::TextComponent(comp) => Some(comp),
40 Component::Image(_) => None,
41 })
42 .collect()
43 }
44
45 pub fn components_mut(&mut self) -> Vec<&mut TextComponent> {
46 self.components
47 .iter_mut()
48 .filter_map(|c| match c {
49 Component::TextComponent(comp) => Some(comp),
50 Component::Image(_) => None,
51 })
52 .collect()
53 }
54
55 #[must_use]
56 pub fn file_name(&self) -> Option<&str> {
57 self.file_name.as_deref()
58 }
59
60 #[must_use]
61 pub fn words(&self) -> Vec<&Word> {
62 self.components
63 .iter()
64 .filter_map(|c| match c {
65 Component::TextComponent(comp) => Some(comp),
66 Component::Image(_) => None,
67 })
68 .flat_map(|c| c.content().iter().flatten())
69 .collect()
70 }
71
72 pub fn find_and_mark(&mut self, search: &str) {
73 let mut words = self
74 .components
75 .iter_mut()
76 .filter_map(|c| match c {
77 Component::TextComponent(comp) => Some(comp),
78 Component::Image(_) => None,
79 })
80 .flat_map(|c| c.words_mut())
81 .collect::<Vec<_>>();
82 find_and_mark(search, &mut words);
83 }
84
85 #[must_use]
86 pub fn search_results_heights(&self) -> Vec<usize> {
87 self.components
88 .iter()
89 .filter_map(|c| match c {
90 Component::TextComponent(comp) => Some(comp),
91 Component::Image(_) => None,
92 })
93 .flat_map(|c| {
94 let mut heights = c.selected_heights();
95 heights.iter_mut().for_each(|h| *h += c.y_offset() as usize);
96 heights
97 })
98 .collect()
99 }
100
101 pub fn clear(&mut self) {
102 self.file_name = None;
103 self.components.clear();
104 }
105
106 pub fn select(&mut self, index: usize) -> Result<u16, String> {
107 self.deselect();
108 self.is_focused = true;
109 let mut count = 0;
110 for comp in self.components.iter_mut().filter_map(|f| match f {
111 Component::TextComponent(comp) => Some(comp),
112 Component::Image(_) => None,
113 }) {
114 let link_inside_comp = index - count < comp.num_links();
115 if link_inside_comp {
116 comp.visually_select(index - count)?;
117 return Ok(comp.y_offset());
118 }
119 count += comp.num_links();
120 }
121 Err(format!("Index out of bounds: {index} >= {count}"))
122 }
123
124 pub fn deselect(&mut self) {
125 self.is_focused = false;
126 for comp in self.components.iter_mut().filter_map(|f| match f {
127 Component::TextComponent(comp) => Some(comp),
128 Component::Image(_) => None,
129 }) {
130 comp.deselect();
131 }
132 }
133
134 #[must_use]
135 pub fn find_footnote(&self, search: &str) -> String {
136 let footnote = self
137 .components
138 .iter()
139 .filter_map(|f| match f {
140 Component::TextComponent(text_component) => {
141 if text_component.kind() == TextNode::Footnote {
142 Some(text_component)
143 } else {
144 None
145 }
146 }
147 Component::Image(_) => None,
148 })
149 .filter(|f| {
150 if let Some(foot_ref) = f.meta_info().iter().next() {
151 foot_ref.content() == search
152 } else {
153 false
154 }
155 })
156 .flat_map(|f| f.content().iter().flatten())
157 .filter(|f| f.kind() == WordType::Footnote)
158 .map(Word::content)
159 .collect::<String>();
160
161 if footnote.is_empty() {
162 String::from("Footnote not found")
163 } else {
164 footnote
165 }
166 }
167
168 #[must_use]
169 pub fn link_index_and_height(&self) -> Vec<(usize, u16)> {
170 let mut indexes = Vec::new();
171 let mut count = 0;
172 self.components
173 .iter()
174 .filter_map(|f| match f {
175 Component::TextComponent(comp) => Some(comp),
176 Component::Image(_) => None,
177 })
178 .for_each(|comp| {
179 let height = comp.y_offset();
180 comp.content().iter().enumerate().for_each(|(index, row)| {
181 row.iter().for_each(|c| {
182 if matches!(
183 c.kind(),
184 WordType::Link | WordType::Selected | WordType::FootnoteInline
185 ) {
186 indexes.push((count, height + index as u16));
187 count += 1;
188 }
189 });
190 });
191 });
192
193 indexes
194 }
195
196 pub fn set_scroll(&mut self, scroll: u16) {
198 let mut y_offset = 0;
199 for component in &mut self.components {
200 component.set_y_offset(y_offset);
201 component.set_scroll_offset(scroll);
202 y_offset += component.height();
203 }
204 }
205
206 pub fn heading_offset(&self, heading: &str) -> Result<u16, String> {
207 let mut y_offset = 0;
208 for component in &self.components {
209 match component {
210 Component::TextComponent(comp) => {
211 if comp.kind() == TextNode::Heading
212 && compare_heading(&heading[1..], comp.content())
213 {
214 return Ok(y_offset);
215 }
216 y_offset += comp.height();
217 }
218 Component::Image(e) => y_offset += e.height(),
219 }
220 }
221 Err(format!("Heading not found: {heading}"))
222 }
223
224 #[must_use]
226 pub fn content(&self) -> Vec<String> {
227 self.components()
228 .iter()
229 .flat_map(|c| c.content_as_lines())
230 .collect()
231 }
232
233 #[must_use]
234 pub fn selected(&self) -> &str {
235 let block = self
236 .components
237 .iter()
238 .filter_map(|f| match f {
239 Component::TextComponent(comp) => Some(comp),
240 Component::Image(_) => None,
241 })
242 .find(|c| c.is_focused())
243 .unwrap();
244 block.highlight_link().unwrap()
245 }
246
247 #[must_use]
248 pub fn selected_underlying_type(&self) -> WordType {
249 let selected = self
250 .components
251 .iter()
252 .filter_map(|f| match f {
253 Component::TextComponent(comp) => Some(comp),
254 Component::Image(_) => None,
255 })
256 .find(|c| c.is_focused())
257 .unwrap()
258 .content()
259 .iter()
260 .flatten()
261 .filter(|c| c.kind() == WordType::Selected)
262 .collect::<Vec<_>>();
263
264 selected.first().unwrap().previous_type()
265 }
266
267 pub fn transform(&mut self, width: u16) {
269 for component in self.components_mut() {
270 component.transform(width);
271 }
272 }
273
274 #[must_use]
276 pub fn add_missing_components(self) -> Self {
277 let mut components = Vec::new();
278 let mut iter = self.components.into_iter().peekable();
279 while let Some(component) = iter.next() {
280 let kind = component.kind();
281 components.push(component);
282 if let Some(next) = iter.peek()
283 && kind != TextNode::LineBreak
284 && next.kind() != TextNode::LineBreak
285 {
286 components.push(Component::TextComponent(TextComponent::new(
287 TextNode::LineBreak,
288 Vec::new(),
289 )));
290 }
291 }
292 Self {
293 file_name: self.file_name,
294 components,
295 is_focused: self.is_focused,
296 }
297 }
298
299 #[must_use]
300 pub fn height(&self) -> u16 {
301 self.components.iter().map(ComponentProps::height).sum()
302 }
303
304 #[must_use]
305 pub fn num_links(&self) -> usize {
306 self.components
307 .iter()
308 .filter_map(|f| match f {
309 Component::TextComponent(comp) => Some(comp),
310 Component::Image(_) => None,
311 })
312 .map(TextComponent::num_links)
313 .sum()
314 }
315}
316
317pub trait ComponentProps {
318 fn height(&self) -> u16;
319 fn set_y_offset(&mut self, y_offset: u16);
320 fn set_scroll_offset(&mut self, scroll: u16);
321 fn kind(&self) -> TextNode;
322}
323
324pub enum Component {
325 TextComponent(TextComponent),
326 Image(ImageComponent),
327}
328
329impl From<TextComponent> for Component {
330 fn from(comp: TextComponent) -> Self {
331 Component::TextComponent(comp)
332 }
333}
334
335impl ComponentProps for Component {
336 fn height(&self) -> u16 {
337 match self {
338 Component::TextComponent(comp) => comp.height(),
339 Component::Image(comp) => comp.height(),
340 }
341 }
342
343 fn set_y_offset(&mut self, y_offset: u16) {
344 match self {
345 Component::TextComponent(comp) => comp.set_y_offset(y_offset),
346 Component::Image(comp) => comp.set_y_offset(y_offset),
347 }
348 }
349
350 fn set_scroll_offset(&mut self, scroll: u16) {
351 match self {
352 Component::TextComponent(comp) => comp.set_scroll_offset(scroll),
353 Component::Image(comp) => comp.set_scroll_offset(scroll),
354 }
355 }
356
357 fn kind(&self) -> TextNode {
358 match self {
359 Component::TextComponent(comp) => comp.kind(),
360 Component::Image(comp) => comp.kind(),
361 }
362 }
363}