Skip to main content

cadmus_core/view/dictionary/
mod.rs

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('<', "&lt;").replace('>', "&gt;")
55            ));
56        }
57        out.push_str(&body[html_start..]);
58        out
59    } else {
60        format!(
61            "<pre>{}</pre>",
62            body.replace('<', "&lt;").replace('>', "&gt;")
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('<', "&lt;").replace('>', "&gt;")
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('<', "&lt;").replace('>', "&gt;")
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}