1mod bottom_bar;
2
3use self::bottom_bar::BottomBar;
4use crate::color::BLACK;
5use crate::context::Context;
6use crate::device::CURRENT_DEVICE;
7use crate::document::html::Html5Document;
8use crate::document::{Document, Location};
9use crate::font::Fonts;
10use crate::framebuffer::{Framebuffer, Pixmap, UpdateMode};
11use crate::geom::{halves, CycleDir, Dir, Point, Rectangle};
12use crate::gesture::GestureEvent;
13use crate::input::{ButtonCode, ButtonStatus, DeviceEvent};
14use crate::unit::scale_by_dpi;
15use crate::view::common::{locate, locate_by_id};
16use crate::view::common::{toggle_battery_menu, toggle_clock_menu, toggle_main_menu};
17use crate::view::filler::Filler;
18use crate::view::image::Image;
19use crate::view::keyboard::Keyboard;
20use crate::view::menu::{Menu, MenuKind};
21use crate::view::named_input::NamedInput;
22use crate::view::search_bar::SearchBar;
23use crate::view::top_bar::{TopBar, TopBarVariant};
24use crate::view::{Bus, Event, Hub, RenderData, RenderQueue, View};
25use crate::view::{EntryId, EntryKind, Id, ViewId, ID_FEEDER};
26use crate::view::{BIG_BAR_HEIGHT, SMALL_BAR_HEIGHT, THICKNESS_MEDIUM};
27use regex::Regex;
28use tracing::error;
29
30const VIEWER_STYLESHEET: &str = "css/dictionary.css";
31const USER_STYLESHEET: &str = "css/dictionary-user.css";
32
33pub struct Dictionary {
34 id: Id,
35 rect: Rectangle,
36 children: Vec<Box<dyn View>>,
37 doc: Html5Document,
38 location: usize,
39 fuzzy: bool,
40 query: String,
41 language: String,
42 target: Option<String>,
43 focus: Option<ViewId>,
44}
45
46fn format_body(body: &str) -> String {
47 let body = body.trim_end_matches('\n').trim_end_matches("</html>");
48 if let Some(html_start) = body.find('<') {
49 let prefix = body[..html_start].trim();
50 let mut out = String::new();
51 if !prefix.is_empty() {
52 out.push_str(&format!(
53 "<p>{}</p>\n",
54 prefix.replace('<', "<").replace('>', ">")
55 ));
56 }
57 out.push_str(&body[html_start..]);
58 out
59 } else {
60 format!(
61 "<pre>{}</pre>",
62 body.replace('<', "<").replace('>', ">")
63 )
64 }
65}
66
67#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
68fn query_to_content(
69 query: &str,
70 language: &String,
71 fuzzy: bool,
72 target: Option<&String>,
73 context: &mut Context,
74) -> String {
75 let mut content = String::new();
76
77 for (name, dict) in context.dictionaries.iter_mut() {
78 if target.is_some() && target != Some(name) {
79 continue;
80 }
81
82 if target.is_none()
83 && !language.is_empty()
84 && context.settings.dictionary.languages.contains_key(name)
85 && !context.settings.dictionary.languages[name].contains(language)
86 {
87 continue;
88 }
89
90 #[cfg(feature = "tracing")]
91 let _dict_span =
92 tracing::info_span!("dictionary_lookup", dictionary_name = %name).entered();
93
94 if let Some(results) = dict
95 .lookup(query, fuzzy)
96 .map_err(|e| error!("Can't search dictionary: {:#}.", e))
97 .ok()
98 .filter(|r| !r.is_empty())
99 {
100 if target.is_none() {
101 content.push_str(&format!(
102 "<h1 class=\"dictname\">{}</h1>\n",
103 name.replace('<', "<").replace('>', ">")
104 ));
105 }
106 for [head, body] in results {
107 if !body.trim_start().starts_with("<h2") {
108 content.push_str(&format!(
109 "<h2 class=\"headword\">{}</h2>\n",
110 head.replace('<', "<").replace('>', ">")
111 ));
112 }
113 content.push_str(&format_body(&body));
114 content.push('\n');
115 }
116 }
117 }
118
119 if content.is_empty() {
120 if context.dictionaries.is_empty() {
121 content.push_str("<p class=\"info\">No dictionaries present.</p>");
122 } else {
123 content.push_str("<p class=\"info\">No definitions found.</p>");
124 }
125 }
126
127 content
128}
129
130impl Dictionary {
131 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
132 pub fn new(
133 rect: Rectangle,
134 query: &str,
135 language: &str,
136 hub: &Hub,
137 rq: &mut RenderQueue,
138 context: &mut Context,
139 ) -> Dictionary {
140 let id = ID_FEEDER.next();
141 let mut children = Vec::new();
142 let dpi = CURRENT_DEVICE.dpi;
143 let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
144 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
145 let (small_thickness, big_thickness) = halves(thickness);
146
147 let top_bar = TopBar::new(
148 rect![
149 rect.min.x,
150 rect.min.y,
151 rect.max.x,
152 rect.min.y + small_height - small_thickness
153 ],
154 TopBarVariant::Back,
155 "Dictionary".to_string(),
156 context,
157 );
158 children.push(Box::new(top_bar) as Box<dyn View>);
159
160 let separator = Filler::new(
161 rect![
162 rect.min.x,
163 rect.min.y + small_height - small_thickness,
164 rect.max.x,
165 rect.min.y + small_height + big_thickness
166 ],
167 BLACK,
168 );
169 children.push(Box::new(separator) as Box<dyn View>);
170
171 let search_bar = SearchBar::new(
172 rect![
173 rect.min.x,
174 rect.min.y + small_height + big_thickness,
175 rect.max.x,
176 rect.min.y + 2 * small_height - small_thickness
177 ],
178 ViewId::DictionarySearchInput,
179 "",
180 query,
181 context,
182 );
183 children.push(Box::new(search_bar) as Box<dyn View>);
184
185 let separator = Filler::new(
186 rect![
187 rect.min.x,
188 rect.min.y + 2 * small_height - small_thickness,
189 rect.max.x,
190 rect.min.y + 2 * small_height + big_thickness
191 ],
192 BLACK,
193 );
194 children.push(Box::new(separator) as Box<dyn View>);
195
196 let langs = &context.settings.dictionary.languages;
197 let matches = context
198 .dictionaries
199 .keys()
200 .filter(|&k| langs.contains_key(k) && langs[k].contains(&language.to_string()))
201 .collect::<Vec<&String>>();
202 let target = if matches.len() == 1 {
203 Some(matches[0].clone())
204 } else {
205 if context.dictionaries.len() == 1 {
206 Some(context.dictionaries.keys().next().cloned().unwrap())
207 } else {
208 None
209 }
210 };
211
212 let image_rect = rect![
213 rect.min.x,
214 rect.min.y + 2 * small_height + big_thickness,
215 rect.max.x,
216 rect.max.y - small_height - small_thickness
217 ];
218
219 let image = Image::new(image_rect, Pixmap::new(1, 1, 1));
220 children.push(Box::new(image) as Box<dyn View>);
221
222 let mut doc = Html5Document::new_from_memory("");
223 doc.layout(
224 image_rect.width(),
225 image_rect.height(),
226 context.settings.dictionary.font_size,
227 dpi,
228 );
229 doc.set_margin_width(context.settings.dictionary.margin_width);
230 doc.set_viewer_stylesheet(VIEWER_STYLESHEET);
231 doc.set_user_stylesheet(USER_STYLESHEET);
232
233 let separator = Filler::new(
234 rect![
235 rect.min.x,
236 rect.max.y - small_height - small_thickness,
237 rect.max.x,
238 rect.max.y - small_height + big_thickness
239 ],
240 BLACK,
241 );
242 children.push(Box::new(separator) as Box<dyn View>);
243
244 let bottom_bar = BottomBar::new(
245 rect![
246 rect.min.x,
247 rect.max.y - small_height + big_thickness,
248 rect.max.x,
249 rect.max.y
250 ],
251 target.as_deref().unwrap_or("All"),
252 false,
253 false,
254 );
255 children.push(Box::new(bottom_bar) as Box<dyn View>);
256
257 rq.add(RenderData::new(id, rect, UpdateMode::Gui));
258
259 if query.is_empty() {
260 hub.send(Event::Focus(Some(ViewId::DictionarySearchInput)))
261 .ok();
262 } else {
263 hub.send(Event::Define(query.to_string())).ok();
264 }
265
266 Dictionary {
267 id,
268 rect,
269 children,
270 doc,
271 location: 0,
272 fuzzy: false,
273 query: query.to_string(),
274 language: language.to_string(),
275 target,
276 focus: None,
277 }
278 }
279
280 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, rq, context)))]
281 pub fn toggle_title_menu(
282 &mut self,
283 rect: Rectangle,
284 enable: Option<bool>,
285 rq: &mut RenderQueue,
286 context: &mut Context,
287 ) {
288 if let Some(index) = locate_by_id(self, ViewId::TitleMenu) {
289 if let Some(true) = enable {
290 return;
291 }
292
293 rq.add(RenderData::expose(
294 *self.child(index).rect(),
295 UpdateMode::Gui,
296 ));
297 self.children.remove(index);
298 } else {
299 if let Some(false) = enable {
300 return;
301 }
302 let entries = vec![EntryKind::Command(
303 "Reload Dictionaries".to_string(),
304 EntryId::ReloadDictionaries,
305 )];
306 let title_menu = Menu::new(
307 rect,
308 ViewId::TitleMenu,
309 MenuKind::DropDown,
310 entries,
311 context,
312 );
313 rq.add(RenderData::new(
314 title_menu.id(),
315 *title_menu.rect(),
316 UpdateMode::Gui,
317 ));
318 self.children.push(Box::new(title_menu) as Box<dyn View>);
319 }
320 }
321
322 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, rq, context)))]
323 fn toggle_search_menu(
324 &mut self,
325 rect: Rectangle,
326 enable: Option<bool>,
327 rq: &mut RenderQueue,
328 context: &mut Context,
329 ) {
330 if let Some(index) = locate_by_id(self, ViewId::SearchMenu) {
331 if let Some(true) = enable {
332 return;
333 }
334
335 rq.add(RenderData::expose(
336 *self.child(index).rect(),
337 UpdateMode::Gui,
338 ));
339 self.children.remove(index);
340 } else {
341 if let Some(false) = enable {
342 return;
343 }
344 let entries = vec![EntryKind::CheckBox(
345 "Fuzzy".to_string(),
346 EntryId::ToggleFuzzy,
347 self.fuzzy,
348 )];
349 let search_menu = Menu::new(
350 rect,
351 ViewId::SearchMenu,
352 MenuKind::Contextual,
353 entries,
354 context,
355 );
356 rq.add(RenderData::new(
357 search_menu.id(),
358 *search_menu.rect(),
359 UpdateMode::Gui,
360 ));
361 self.children.push(Box::new(search_menu) as Box<dyn View>);
362 }
363 }
364
365 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, rq, context)))]
366 fn toggle_search_target_menu(
367 &mut self,
368 rect: Rectangle,
369 enable: Option<bool>,
370 rq: &mut RenderQueue,
371 context: &mut Context,
372 ) {
373 if let Some(index) = locate_by_id(self, ViewId::SearchTargetMenu) {
374 if let Some(true) = enable {
375 return;
376 }
377
378 rq.add(RenderData::expose(
379 *self.child(index).rect(),
380 UpdateMode::Gui,
381 ));
382 self.children.remove(index);
383 } else {
384 if let Some(false) = enable {
385 return;
386 }
387 let mut entries = context
388 .dictionaries
389 .keys()
390 .map(|k| {
391 EntryKind::RadioButton(
392 k.to_string(),
393 EntryId::SetSearchTarget(Some(k.to_string())),
394 self.target == Some(k.to_string()),
395 )
396 })
397 .collect::<Vec<EntryKind>>();
398 if !entries.is_empty() {
399 entries.push(EntryKind::Separator);
400 }
401 entries.push(EntryKind::RadioButton(
402 "All".to_string(),
403 EntryId::SetSearchTarget(None),
404 self.target.is_none(),
405 ));
406 let search_target_menu = Menu::new(
407 rect,
408 ViewId::SearchTargetMenu,
409 MenuKind::DropDown,
410 entries,
411 context,
412 );
413 rq.add(RenderData::new(
414 search_target_menu.id(),
415 *search_target_menu.rect(),
416 UpdateMode::Gui,
417 ));
418 self.children
419 .push(Box::new(search_target_menu) as Box<dyn View>);
420 }
421 }
422
423 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, hub, rq, context)))]
424 fn toggle_keyboard(
425 &mut self,
426 enable: bool,
427 id: Option<ViewId>,
428 hub: &Hub,
429 rq: &mut RenderQueue,
430 context: &mut Context,
431 ) {
432 if let Some(index) = locate::<Keyboard>(self) {
433 if enable {
434 return;
435 }
436
437 let mut rect = *self.child(index).rect();
438 rect.absorb(self.child(index - 1).rect());
439 self.children.drain(index - 1..=index);
440
441 context.kb_rect = Rectangle::default();
442 rq.add(RenderData::expose(rect, UpdateMode::Gui));
443 hub.send(Event::Focus(None)).ok();
444 } else {
445 if !enable {
446 return;
447 }
448
449 let dpi = CURRENT_DEVICE.dpi;
450 let (small_height, big_height) = (
451 scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32,
452 scale_by_dpi(BIG_BAR_HEIGHT, dpi) as i32,
453 );
454 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
455 let (small_thickness, big_thickness) = halves(thickness);
456
457 let mut kb_rect = rect![
458 self.rect.min.x,
459 self.rect.max.y - (small_height + 3 * big_height) as i32 + big_thickness,
460 self.rect.max.x,
461 self.rect.max.y - small_height - small_thickness
462 ];
463
464 let number = id == Some(ViewId::GoToPageInput);
465 let index = locate::<BottomBar>(self).unwrap() + 1;
466
467 let keyboard = Keyboard::new(&mut kb_rect, number, context);
468 self.children
469 .insert(index, Box::new(keyboard) as Box<dyn View>);
470
471 let separator = Filler::new(
472 rect![
473 self.rect.min.x,
474 kb_rect.min.y - thickness,
475 self.rect.max.x,
476 kb_rect.min.y
477 ],
478 BLACK,
479 );
480 self.children
481 .insert(index, Box::new(separator) as Box<dyn View>);
482
483 for i in index..=index + 1 {
484 rq.add(RenderData::new(
485 self.child(i).id(),
486 *self.child(i).rect(),
487 UpdateMode::Gui,
488 ));
489 }
490 }
491 }
492
493 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, hub, rq, context)))]
494 fn toggle_edit_languages(
495 &mut self,
496 enable: Option<bool>,
497 hub: &Hub,
498 rq: &mut RenderQueue,
499 context: &mut Context,
500 ) {
501 if let Some(index) = locate_by_id(self, ViewId::EditLanguages) {
502 if let Some(true) = enable {
503 return;
504 }
505
506 rq.add(RenderData::expose(
507 *self.child(index).rect(),
508 UpdateMode::Gui,
509 ));
510 self.children.remove(index);
511
512 if self
513 .focus
514 .map(|focus_id| focus_id == ViewId::EditLanguagesInput)
515 .unwrap_or(false)
516 {
517 self.toggle_keyboard(false, None, hub, rq, context);
518 }
519 } else {
520 if let Some(false) = enable {
521 return;
522 }
523
524 let mut edit_languages = NamedInput::new(
525 "Languages".to_string(),
526 ViewId::EditLanguages,
527 ViewId::EditLanguagesInput,
528 16,
529 context,
530 );
531 if let Some(langs) = self
532 .target
533 .as_ref()
534 .and_then(|name| context.settings.dictionary.languages.get(name))
535 .filter(|langs| !langs.is_empty())
536 {
537 edit_languages.set_text(&langs.join(", "), &mut RenderQueue::new(), context);
538 }
539
540 rq.add(RenderData::new(
541 edit_languages.id(),
542 *edit_languages.rect(),
543 UpdateMode::Gui,
544 ));
545 hub.send(Event::Focus(Some(ViewId::EditLanguagesInput)))
546 .ok();
547
548 self.children
549 .push(Box::new(edit_languages) as Box<dyn View>);
550 }
551 }
552
553 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, rq, context)))]
554 fn reseed(&mut self, rq: &mut RenderQueue, context: &mut Context) {
555 if let Some(top_bar) = self.child_mut(0).downcast_mut::<TopBar>() {
556 top_bar.reseed(rq, context);
557 }
558
559 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Gui));
560 }
561
562 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, rq), fields(dir = ?dir)))]
563 fn go_to_neighbor(&mut self, dir: CycleDir, rq: &mut RenderQueue) {
564 let location = match dir {
565 CycleDir::Previous => Location::Previous(self.location),
566 CycleDir::Next => Location::Next(self.location),
567 };
568 if let Some(image) = self.children[4].downcast_mut::<Image>() {
569 if let Some((pixmap, loc)) =
570 self.doc
571 .pixmap(location, 1.0, CURRENT_DEVICE.color_samples())
572 {
573 image.update(pixmap, rq);
574 self.location = loc;
575 }
576 }
577 if let Some(bottom_bar) = self.children[6].downcast_mut::<BottomBar>() {
578 bottom_bar.update_icons(
579 self.doc
580 .resolve_location(Location::Previous(self.location))
581 .is_some(),
582 self.doc
583 .resolve_location(Location::Next(self.location))
584 .is_some(),
585 rq,
586 );
587 }
588 }
589
590 #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
591 fn define(&mut self, text: Option<&str>, rq: &mut RenderQueue, context: &mut Context) {
592 if let Some(query) = text {
593 self.query = query.to_string();
594 if let Some(search_bar) = self.children[2].downcast_mut::<SearchBar>() {
595 search_bar.set_text(query, rq, context);
596 }
597 }
598 let content = query_to_content(
599 &self.query,
600 &self.language,
601 self.fuzzy,
602 self.target.as_ref(),
603 context,
604 );
605 self.doc.update(&content);
606 if let Some(image) = self.children[4].downcast_mut::<Image>() {
607 if let Some((pixmap, loc)) =
608 self.doc
609 .pixmap(Location::Exact(0), 1.0, CURRENT_DEVICE.color_samples())
610 {
611 image.update(pixmap, rq);
612 self.location = loc;
613 }
614 }
615 if let Some(bottom_bar) = self.children[6].downcast_mut::<BottomBar>() {
616 bottom_bar.update_icons(
617 false,
618 self.doc
619 .resolve_location(Location::Next(self.location))
620 .is_some(),
621 rq,
622 );
623 }
624 }
625
626 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self), fields(pt = ?pt)))]
627 fn underlying_word(&mut self, pt: Point) -> Option<String> {
628 let dpi = CURRENT_DEVICE.dpi;
629 let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
630 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
631 let (_, big_thickness) = halves(thickness);
632 let offset = pt!(
633 self.rect.min.x,
634 self.rect.min.y + 2 * small_height + big_thickness
635 );
636
637 if let Some((words, _)) = self.doc.words(Location::Exact(self.location)) {
638 for word in words {
639 let rect = word.rect.to_rect() + offset;
640 if rect.includes(pt) {
641 return Some(word.text);
642 }
643 }
644 }
645
646 None
647 }
648
649 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, rq, context), fields(pt = ?pt)))]
650 fn follow_link(&mut self, pt: Point, rq: &mut RenderQueue, context: &mut Context) {
651 let dpi = CURRENT_DEVICE.dpi;
652 let small_height = scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32;
653 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
654 let (_, big_thickness) = halves(thickness);
655 let offset = pt!(
656 self.rect.min.x,
657 self.rect.min.y + 2 * small_height + big_thickness
658 );
659
660 if let Some((links, _)) = self.doc.links(Location::Exact(self.location)) {
661 for link in links {
662 let rect = link.rect.to_rect() + offset;
663 if rect.includes(pt) && link.text.starts_with('?') {
664 self.define(Some(&link.text[1..]), rq, context);
665 return;
666 }
667 }
668 }
669
670 let half_width = self.rect.width() as i32 / 2;
671 if pt.x - offset.x < half_width {
672 self.go_to_neighbor(CycleDir::Previous, rq);
673 } else {
674 self.go_to_neighbor(CycleDir::Next, rq);
675 }
676 }
677}
678
679impl View for Dictionary {
680 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, hub, _bus, rq, context), fields(event = ?evt), ret(level=tracing::Level::TRACE)))]
681 fn handle_event(
682 &mut self,
683 evt: &Event,
684 hub: &Hub,
685 _bus: &mut Bus,
686 rq: &mut RenderQueue,
687 context: &mut Context,
688 ) -> bool {
689 match *evt {
690 Event::Define(ref query) => {
691 self.define(Some(query), rq, context);
692 true
693 }
694 Event::Submit(ViewId::DictionarySearchInput, ref text) => {
695 if !text.is_empty() {
696 self.toggle_keyboard(false, None, hub, rq, context);
697 self.define(Some(text), rq, context);
698 }
699 true
700 }
701 Event::Page(dir) => {
702 self.go_to_neighbor(dir, rq);
703 true
704 }
705 Event::Gesture(GestureEvent::Swipe { dir, start, .. }) if self.rect.includes(start) => {
706 match dir {
707 Dir::West => self.go_to_neighbor(CycleDir::Next, rq),
708 Dir::East => self.go_to_neighbor(CycleDir::Previous, rq),
709 _ => (),
710 }
711 true
712 }
713 Event::Device(DeviceEvent::Button {
714 code,
715 status: ButtonStatus::Released,
716 ..
717 }) => {
718 let cd = match code {
719 ButtonCode::Backward => Some(CycleDir::Previous),
720 ButtonCode::Forward => Some(CycleDir::Next),
721 _ => None,
722 };
723 if let Some(cd) = cd {
724 let loc = self.location;
725 self.go_to_neighbor(cd, rq);
726 if self.location == loc {
727 hub.send(Event::Back).ok();
728 }
729 }
730 true
731 }
732 Event::Gesture(GestureEvent::Tap(center)) if self.rect.includes(center) => {
733 self.follow_link(center, rq, context);
734 true
735 }
736 Event::Gesture(GestureEvent::HoldFingerLong(pt, _)) => {
737 if let Some(text) = self.underlying_word(pt) {
738 let query = text
739 .trim_matches(|c: char| !c.is_alphanumeric())
740 .to_string();
741 self.define(Some(&query), rq, context);
742 }
743 true
744 }
745 Event::Select(EntryId::SetSearchTarget(ref target)) => {
746 if *target != self.target {
747 self.target = target.clone();
748 let name = self.target.as_deref().unwrap_or("All");
749 if let Some(bottom_bar) = self.children[6].downcast_mut::<BottomBar>() {
750 bottom_bar.update_name(name, rq);
751 }
752 if !self.query.is_empty() {
753 self.define(None, rq, context);
754 }
755 }
756 true
757 }
758 Event::Select(EntryId::ToggleFuzzy) => {
759 self.fuzzy = !self.fuzzy;
760 if !self.query.is_empty() {
761 self.define(None, rq, context);
762 }
763 true
764 }
765 Event::Select(EntryId::ReloadDictionaries) => {
766 context.dictionaries.clear();
767 context.load_dictionaries();
768 if let Some(name) = self.target.as_ref() {
769 if !context.dictionaries.contains_key(name) {
770 self.target = None;
771 if let Some(bottom_bar) = self.child_mut(6).downcast_mut::<BottomBar>() {
772 bottom_bar.update_name("All", rq);
773 }
774 }
775 }
776 true
777 }
778 Event::EditLanguages => {
779 if self.target.is_some() {
780 self.toggle_edit_languages(None, hub, rq, context);
781 }
782 true
783 }
784 Event::Submit(ViewId::EditLanguagesInput, ref text) => {
785 if let Some(name) = self.target.as_ref() {
786 let re = Regex::new(r"\s*,\s*").unwrap();
787 context
788 .settings
789 .dictionary
790 .languages
791 .insert(name.clone(), re.split(text).map(String::from).collect());
792 if self.target.is_none() && !self.query.is_empty() {
793 self.define(None, rq, context);
794 }
795 }
796 true
797 }
798 Event::Close(ViewId::EditLanguages) => {
799 self.toggle_keyboard(false, None, hub, rq, context);
800 false
801 }
802 Event::Close(ViewId::SearchBar) => {
803 hub.send(Event::Back).ok();
804 true
805 }
806 Event::Focus(v) => {
807 self.focus = v;
808 if v.is_some() {
809 self.toggle_keyboard(true, v, hub, rq, context);
810 }
811 true
812 }
813 Event::ToggleNear(ViewId::TitleMenu, rect) => {
814 self.toggle_title_menu(rect, None, rq, context);
815 true
816 }
817 Event::ToggleNear(ViewId::SearchMenu, rect) => {
818 self.toggle_search_menu(rect, None, rq, context);
819 true
820 }
821 Event::ToggleNear(ViewId::SearchTargetMenu, rect) => {
822 self.toggle_search_target_menu(rect, None, rq, context);
823 true
824 }
825 Event::ToggleNear(ViewId::MainMenu, rect) => {
826 toggle_main_menu(self, rect, None, rq, context);
827 true
828 }
829 Event::ToggleNear(ViewId::BatteryMenu, rect) => {
830 toggle_battery_menu(self, rect, None, rq, context);
831 true
832 }
833 Event::ToggleNear(ViewId::ClockMenu, rect) => {
834 toggle_clock_menu(self, rect, None, rq, context);
835 true
836 }
837 Event::Reseed => {
838 self.reseed(rq, context);
839 true
840 }
841 Event::Gesture(GestureEvent::Cross(_)) => {
842 hub.send(Event::Back).ok();
843 true
844 }
845 _ => false,
846 }
847 }
848
849 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _fb, _fonts, _rect), fields(rect = ?_rect)))]
850 fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
851
852 #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, hub, rq, context), fields(rect = ?rect)))]
853 fn resize(&mut self, rect: Rectangle, hub: &Hub, rq: &mut RenderQueue, context: &mut Context) {
854 let dpi = CURRENT_DEVICE.dpi;
855 let (small_height, big_height) = (
856 scale_by_dpi(SMALL_BAR_HEIGHT, dpi) as i32,
857 scale_by_dpi(BIG_BAR_HEIGHT, dpi) as i32,
858 );
859 let thickness = scale_by_dpi(THICKNESS_MEDIUM, dpi) as i32;
860 let (small_thickness, big_thickness) = halves(thickness);
861
862 self.children[0].resize(
863 rect![
864 rect.min.x,
865 rect.min.y,
866 rect.max.x,
867 rect.min.y + small_height - small_thickness
868 ],
869 hub,
870 rq,
871 context,
872 );
873
874 self.children[1].resize(
875 rect![
876 rect.min.x,
877 rect.min.y + small_height - small_thickness,
878 rect.max.x,
879 rect.min.y + small_height + big_thickness
880 ],
881 hub,
882 rq,
883 context,
884 );
885
886 self.children[2].resize(
887 rect![
888 rect.min.x,
889 rect.min.y + small_height + big_thickness,
890 rect.max.x,
891 rect.min.y + 2 * small_height - small_thickness
892 ],
893 hub,
894 rq,
895 context,
896 );
897
898 self.children[3].resize(
899 rect![
900 rect.min.x,
901 rect.min.y + 2 * small_height - small_thickness,
902 rect.max.x,
903 rect.min.y + 2 * small_height + big_thickness
904 ],
905 hub,
906 rq,
907 context,
908 );
909
910 let image_rect = rect![
911 rect.min.x,
912 rect.min.y + 2 * small_height + big_thickness,
913 rect.max.x,
914 rect.max.y - small_height - small_thickness
915 ];
916 self.doc.layout(
917 image_rect.width(),
918 image_rect.height(),
919 context.settings.dictionary.font_size,
920 dpi,
921 );
922 if let Some(image) = self.children[4].downcast_mut::<Image>() {
923 if let Some((pixmap, loc)) = self.doc.pixmap(
924 Location::Exact(self.location),
925 1.0,
926 CURRENT_DEVICE.color_samples(),
927 ) {
928 image.update(pixmap, &mut RenderQueue::new());
929 self.location = loc;
930 }
931 }
932 self.children[4].resize(image_rect, hub, rq, context);
933
934 self.children[5].resize(
935 rect![
936 rect.min.x,
937 rect.max.y - small_height - small_thickness,
938 rect.max.x,
939 rect.max.y - small_height + big_thickness
940 ],
941 hub,
942 rq,
943 context,
944 );
945
946 self.children[6].resize(
947 rect![
948 rect.min.x,
949 rect.max.y - small_height + big_thickness,
950 rect.max.x,
951 rect.max.y
952 ],
953 hub,
954 rq,
955 context,
956 );
957 if let Some(bottom_bar) = self.children[6].downcast_mut::<BottomBar>() {
958 bottom_bar.update_icons(
959 self.doc
960 .resolve_location(Location::Previous(self.location))
961 .is_some(),
962 self.doc
963 .resolve_location(Location::Next(self.location))
964 .is_some(),
965 &mut RenderQueue::new(),
966 );
967 }
968 let mut index = 7;
969 if self.len() >= 9 {
970 if self.children[8].is::<Keyboard>() {
971 let kb_rect = rect![
972 rect.min.x,
973 rect.max.y - (small_height + 3 * big_height) as i32 + big_thickness,
974 rect.max.x,
975 rect.max.y - small_height - small_thickness
976 ];
977 self.children[8].resize(kb_rect, hub, rq, context);
978 let kb_rect = *self.children[8].rect();
979 self.children[7].resize(
980 rect![
981 rect.min.x,
982 kb_rect.min.y - thickness,
983 rect.max.x,
984 kb_rect.min.y
985 ],
986 hub,
987 rq,
988 context,
989 );
990 index = 9;
991 }
992 }
993
994 for i in index..self.children.len() {
995 self.children[i].resize(rect, hub, rq, context);
996 }
997
998 self.rect = rect;
999 rq.add(RenderData::new(self.id, self.rect, UpdateMode::Full));
1000 }
1001
1002 fn rect(&self) -> &Rectangle {
1003 &self.rect
1004 }
1005
1006 fn rect_mut(&mut self) -> &mut Rectangle {
1007 &mut self.rect
1008 }
1009
1010 fn children(&self) -> &Vec<Box<dyn View>> {
1011 &self.children
1012 }
1013
1014 fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
1015 &mut self.children
1016 }
1017
1018 fn id(&self) -> Id {
1019 self.id
1020 }
1021}
1022
1023#[cfg(test)]
1024mod tests {
1025 use super::format_body;
1026
1027 #[test]
1028 fn plain_text_prefix_before_html_is_wrapped_in_paragraph() {
1029 let body = "/ˈdɑkjʊmənt/\n<p><b>Noun</b></p><ol><li>A paper.</li></ol></html>";
1030 let result = format_body(body);
1031 assert!(
1032 result.starts_with("<p>/ˈdɑkjʊmənt/</p>"),
1033 "expected plain-text prefix wrapped in <p>, got: {}",
1034 result
1035 );
1036 assert!(
1037 result.contains("<p><b>Noun</b></p>"),
1038 "expected HTML body preserved, got: {}",
1039 result
1040 );
1041 assert!(
1042 !result.contains("</html>"),
1043 "trailing </html> should be stripped"
1044 );
1045 }
1046
1047 #[test]
1048 fn html_only_body_is_passed_through() {
1049 let body = "<p><b>Verb</b></p><ol><li>To record.</li></ol></html>";
1050 let result = format_body(body);
1051 assert!(result.starts_with("<p><b>Verb</b></p>"));
1052 assert!(!result.contains("</html>"));
1053 }
1054
1055 #[test]
1056 fn plain_text_only_body_is_wrapped_in_pre() {
1057 let body = "A plain text definition with no HTML.";
1058 let result = format_body(body);
1059 assert!(result.starts_with("<pre>"));
1060 assert!(result.ends_with("</pre>"));
1061 }
1062
1063 #[test]
1064 fn body_with_no_prefix_and_no_html_suffix() {
1065 let body = "/prəˌnʌnsiˈeɪʃən/\n<p>Content.</p>";
1066 let result = format_body(body);
1067 assert!(result.contains("<p>/prəˌnʌnsiˈeɪʃən/</p>"));
1068 assert!(result.contains("<p>Content.</p>"));
1069 }
1070
1071 #[test]
1072 fn html_suffix_followed_by_newline_has_no_remaining_html() {
1073 let body = "/ˈdɑkjʊmənt/\n<p><b>Noun</b></p><ol><li>A paper.</li></ol></html>\n";
1074 let result = format_body(body);
1075 assert!(
1076 !result.contains("</html>"),
1077 "trailing </html> should be stripped even when followed by newline"
1078 );
1079 assert!(
1080 result.ends_with("</ol>"),
1081 "content after </html> newline should be preserved"
1082 );
1083 }
1084}