Skip to main content

odox_ui/
find_bar.rs

1//! The bar a search is typed into, under the menu: the query, how many matches
2//! there are, and the way from one to the next. It holds what a person typed
3//! and nothing of the document; the shell asks the view what was found.
4//
5// Author: David M. Anderson
6// Built with AI assistance (Claude, Anthropic)
7
8use eframe::egui::{self, Key, Ui, text::CCursor, text::CCursorRange};
9
10use crate::i18n::{fill, t};
11
12/// Which way to move among the matches.
13#[derive(Clone, Copy, PartialEq, Eq)]
14pub enum Step {
15    /// The match after this one, from the last round to the first.
16    Next,
17    /// The match before this one, from the first round to the last.
18    Previous,
19}
20
21/// The state of the search bar.
22#[derive(Default)]
23pub struct FindBar {
24    open: bool,
25    query: String,
26    /// The field is to take the keyboard, and what it holds be selected, on
27    /// the next frame it is drawn.
28    focus: bool,
29    /// What the view was last asked, so that it is asked again only when the
30    /// query or the document is not what it was.
31    asked: Option<(String, u64)>,
32    /// How many matches the view reported for what it was last asked.
33    pub(crate) count: usize,
34    /// Which of them is current, counted from zero.
35    pub(crate) current: usize,
36    /// The view holds a search it has not been told to forget.
37    pub(crate) searching: bool,
38}
39
40impl FindBar {
41    /// Show the bar and put the keyboard in it.
42    pub fn open(&mut self) {
43        self.open = true;
44        self.focus = true;
45    }
46
47    /// Hide the bar. The query stays for the next time.
48    pub fn close(&mut self) {
49        self.open = false;
50        self.asked = None;
51    }
52
53    /// Whether the bar is showing.
54    pub fn is_open(&self) -> bool {
55        self.open
56    }
57
58    /// What was typed.
59    pub fn query(&self) -> &str {
60        &self.query
61    }
62
63    /// Whether the view has to be asked again, given the document's revision;
64    /// answers `Some(query_changed)` if so, and remembers it was asked.
65    pub fn due(&mut self, revision: u64) -> Option<bool> {
66        let same_query = self.asked.as_ref().is_some_and(|(q, _)| *q == self.query);
67        let same_revision = self.asked.as_ref().is_some_and(|(_, r)| *r == revision);
68        if same_query && same_revision {
69            return None;
70        }
71        self.asked = Some((self.query.clone(), revision));
72        Some(!same_query)
73    }
74
75    /// Draw the bar.
76    pub fn ui(&mut self, ui: &mut Ui) -> Option<Step> {
77        let (count, current) = (self.count, self.current);
78        let mut step = None;
79        ui.horizontal(|ui| {
80            let id = egui::Id::new("odox-find");
81            let mut output = egui::TextEdit::singleline(&mut self.query)
82                .id(id)
83                .hint_text(t("Find"))
84                .desired_width(240.0)
85                .show(ui);
86            if self.focus {
87                self.focus = false;
88                output.response.request_focus();
89                output.state.cursor.set_char_range(Some(CCursorRange::two(
90                    CCursor::new(0),
91                    CCursor::new(self.query.chars().count()),
92                )));
93                output.state.store(ui.ctx(), id);
94            }
95            if output.response.lost_focus() && ui.input(|input| input.key_pressed(Key::Enter)) {
96                step = Some(if ui.input(|input| input.modifiers.shift) {
97                    Step::Previous
98                } else {
99                    Step::Next
100                });
101                output.response.request_focus();
102            }
103            if output.response.lost_focus() && ui.input(|input| input.key_pressed(Key::Escape)) {
104                self.close();
105                return;
106            }
107            if ui.button(t("Previous")).clicked() {
108                step = Some(Step::Previous);
109            }
110            if ui.button(t("Next")).clicked() {
111                step = Some(Step::Next);
112            }
113            if self.query.is_empty() {
114            } else if count == 0 {
115                ui.colored_label(ui.visuals().warn_fg_color, t("No matches"));
116            } else {
117                ui.label(fill(
118                    t("{current} of {count}"),
119                    &[
120                        ("current", &(current + 1).to_string()),
121                        ("count", &count.to_string()),
122                    ],
123                ));
124            }
125            ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
126                if ui.button("×").on_hover_text(t("Close")).clicked() {
127                    self.close();
128                }
129            });
130        });
131        step
132    }
133}