1use eframe::egui::{self, Key, Ui, text::CCursor, text::CCursorRange};
9
10use crate::i18n::{fill, t};
11
12#[derive(Clone, Copy, PartialEq, Eq)]
14pub enum Step {
15 Next,
17 Previous,
19}
20
21#[derive(Clone, Copy, PartialEq, Eq)]
23pub enum Replace {
24 One,
26 All,
28}
29
30#[derive(Default)]
32pub struct Pressed {
33 pub step: Option<Step>,
35 pub replace: Option<Replace>,
37}
38
39#[derive(Default, PartialEq, Eq)]
41enum Focus {
42 #[default]
43 Nowhere,
44 Query,
45 Replacement,
46}
47
48#[derive(Default)]
50pub struct FindBar {
51 open: bool,
52 query: String,
53 focus: Focus,
56 asked: Option<(String, u64)>,
59 replacement: String,
61 message: Option<String>,
62 pub(crate) count: usize,
64 pub(crate) current: usize,
66 pub(crate) searching: bool,
68}
69
70impl FindBar {
71 pub fn open(&mut self) {
73 self.open = true;
74 self.focus = Focus::Query;
75 }
76
77 pub fn open_replacing(&mut self) {
80 self.open();
81 if !self.query.is_empty() {
82 self.focus = Focus::Replacement;
83 }
84 }
85
86 pub fn replacement(&self) -> &str {
88 &self.replacement
89 }
90
91 pub fn say(&mut self, message: String) {
93 self.message = Some(message);
94 }
95
96 pub fn close(&mut self) {
98 self.open = false;
99 self.asked = None;
100 }
101
102 pub fn is_open(&self) -> bool {
104 self.open
105 }
106
107 pub fn query(&self) -> &str {
109 &self.query
110 }
111
112 pub fn due(&mut self, revision: u64) -> Option<bool> {
115 let same_query = self.asked.as_ref().is_some_and(|(q, _)| *q == self.query);
116 let same_revision = self.asked.as_ref().is_some_and(|(_, r)| *r == revision);
117 if same_query && same_revision {
118 return None;
119 }
120 self.asked = Some((self.query.clone(), revision));
121 if !same_query {
122 self.message = None;
123 }
124 Some(!same_query)
125 }
126
127 pub fn ui(&mut self, ui: &mut Ui, can_replace: bool) -> Pressed {
129 let (count, current) = (self.count, self.current);
130 let mut pressed = Pressed::default();
131 let step = &mut pressed.step;
132 ui.horizontal(|ui| {
133 let id = egui::Id::new("odox-find");
134 let mut output = egui::TextEdit::singleline(&mut self.query)
135 .id(id)
136 .hint_text(t("Find"))
137 .desired_width(240.0)
138 .show(ui);
139 if self.focus == Focus::Query {
140 self.focus = Focus::Nowhere;
141 output.response.request_focus();
142 output.state.cursor.set_char_range(Some(CCursorRange::two(
143 CCursor::new(0),
144 CCursor::new(self.query.chars().count()),
145 )));
146 output.state.store(ui.ctx(), id);
147 }
148 if output.response.lost_focus() && ui.input(|input| input.key_pressed(Key::Enter)) {
149 *step = Some(if ui.input(|input| input.modifiers.shift) {
150 Step::Previous
151 } else {
152 Step::Next
153 });
154 output.response.request_focus();
155 }
156 if output.response.lost_focus() && ui.input(|input| input.key_pressed(Key::Escape)) {
157 self.close();
158 return;
159 }
160 if ui.button(t("Previous")).clicked() {
161 *step = Some(Step::Previous);
162 }
163 if ui.button(t("Next")).clicked() {
164 *step = Some(Step::Next);
165 }
166 if self.query.is_empty() {
167 } else if count == 0 {
168 ui.colored_label(ui.visuals().warn_fg_color, t("No matches"));
169 } else {
170 ui.label(fill(
171 t("{current} of {count}"),
172 &[
173 ("current", &(current + 1).to_string()),
174 ("count", &count.to_string()),
175 ],
176 ));
177 }
178 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
179 if ui.button("×").on_hover_text(t("Close")).clicked() {
180 self.close();
181 }
182 });
183 });
184 if can_replace && self.open {
185 ui.horizontal(|ui| self.replace_row(ui, count, &mut pressed));
186 }
187 pressed
188 }
189
190 fn replace_row(&mut self, ui: &mut Ui, count: usize, pressed: &mut Pressed) {
191 let id = egui::Id::new("odox-replace");
192 let output = egui::TextEdit::singleline(&mut self.replacement)
193 .id(id)
194 .hint_text(t("Replace with"))
195 .desired_width(240.0)
196 .show(ui);
197 if self.focus == Focus::Replacement {
198 self.focus = Focus::Nowhere;
199 output.response.request_focus();
200 }
201 if output.response.lost_focus() && ui.input(|input| input.key_pressed(Key::Enter)) {
202 pressed.replace = Some(Replace::One);
203 output.response.request_focus();
204 }
205 if output.response.lost_focus() && ui.input(|input| input.key_pressed(Key::Escape)) {
206 self.close();
207 return;
208 }
209 if ui
210 .add_enabled(count > 0, egui::Button::new(t("Replace")))
211 .clicked()
212 {
213 pressed.replace = Some(Replace::One);
214 }
215 if ui
216 .add_enabled(count > 0, egui::Button::new(t("Replace all")))
217 .clicked()
218 {
219 pressed.replace = Some(Replace::All);
220 }
221 if let Some(message) = &self.message {
222 ui.label(message);
223 }
224 }
225}