Skip to content

Commit 986d2ba

Browse files
authored
feat(drawing): improved searching responsiveness (#55)
1 parent 1f6c311 commit 986d2ba

8 files changed

Lines changed: 165 additions & 75 deletions

File tree

src/action/word_picker.rs

Lines changed: 7 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ impl WordPicker {
6363
};
6464

6565
autoreleasepool(|_| {
66-
if let Some((attr_string, _)) = word_picker.get_attributed_string(None, false, "", "") {
66+
if let Some((attr_string, _)) = word_picker.get_attributed_string(None, "", "") {
6767
drawer.draw_attributed_string(attr_string, true);
6868
}
6969
});
@@ -75,17 +75,13 @@ impl WordPicker {
7575
&mut self,
7676
drawer: &UIDrawer,
7777
multi_selection_idx: Option<usize>,
78-
is_searching: bool,
7978
label_prefix: &str,
8079
text_prefix: &str,
8180
) {
8281
autoreleasepool(|_| {
83-
if let Some((attr_string, matched)) = self.get_attributed_string(
84-
multi_selection_idx,
85-
is_searching,
86-
label_prefix,
87-
text_prefix,
88-
) {
82+
if let Some((attr_string, matched)) =
83+
self.get_attributed_string(multi_selection_idx, label_prefix, text_prefix)
84+
{
8985
self.matched = matched;
9086
drawer.draw_attributed_string(attr_string, true);
9187
};
@@ -97,7 +93,6 @@ impl WordPicker {
9793
&self,
9894
width_height_ratio: f64,
9995
multi_selection_idx: Option<usize>,
100-
is_searching: bool,
10196
label_prefix: &str,
10297
text_prefix: &str,
10398
) -> (String, Vec<usize>) {
@@ -168,12 +163,10 @@ impl WordPicker {
168163

169164
let buffer = format!(
170165
"<span class=\"h\">{}</span>\n{buffer}",
171-
if is_searching {
172-
format!("/{}", text_prefix)
173-
} else if !label_prefix.is_empty() && matched.is_empty() {
174-
"Press 󰁮 to go back".into()
166+
if !(label_prefix.is_empty() && text_prefix.is_empty()) && matched.is_empty() {
167+
"Press 󰁮 to return"
175168
} else {
176-
"Press / to search".into()
169+
"Press / to search"
177170
}
178171
);
179172

@@ -202,14 +195,12 @@ impl WordPicker {
202195
fn get_attributed_string(
203196
&self,
204197
multi_selection_idx: Option<usize>,
205-
is_searching: bool,
206198
label_prefix: &str,
207199
text_prefix: &str,
208200
) -> Option<(Retained<NSMutableAttributedString>, Vec<usize>)> {
209201
let (html_str, matched) = self.to_string(
210202
self.screen_ratio,
211203
multi_selection_idx,
212-
is_searching,
213204
label_prefix,
214205
text_prefix,
215206
);

src/app_engine/drawing.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -292,7 +292,6 @@ impl AppEngine {
292292
word_picker.update_text_layer(
293293
&self.drawer,
294294
self.multi_selection.one_side_idx,
295-
self.is_searching,
296295
&self.hint_prefix,
297296
&self.search_prefix,
298297
);

src/app_engine/filtering.rs

Lines changed: 47 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,18 @@ use std::collections::HashSet;
22

33
use super::AppEngine;
44
use crate::{
5-
FilterMode, Mode,
5+
AppSignal, FilterMode, Mode,
66
action::perform_ocr,
7+
app_engine::lifecycle::delay,
78
ax_element::{ElementOfInterest, Target},
89
config::RoleOfInterest,
910
user_interface::{HintBox, hint_boxes_from_frames},
1011
util::{Frame, lower_ascii, select_range_helper},
1112
};
1213
use log::Level;
1314

15+
const DEBOUNCE_TIMEOUT: u64 = 150;
16+
1417
impl AppEngine {
1518
fn ocr_res_filtering(&mut self) {
1619
if self.hint_boxes.is_empty() {
@@ -230,7 +233,11 @@ impl AppEngine {
230233
self.activate(Target::ChildElement);
231234
}
232235
FilterMode::WordPicking => {
233-
if !self.multi_selection.is_on || self.multi_selection.one_side_idx.is_none() {
236+
if !self.search_prefix.is_empty() {
237+
self.search_prefix.clear();
238+
self.draw_word_picker();
239+
} else if !self.multi_selection.is_on || self.multi_selection.one_side_idx.is_none()
240+
{
234241
// Go back to text action menu
235242
self.word_picker = None;
236243
self.draw_element_menu("", RoleOfInterest::PseudoText, true);
@@ -263,26 +270,8 @@ impl AppEngine {
263270
};
264271
}
265272

266-
pub(super) async fn perform_filtering(&mut self, key_char: char, mode: FilterMode) {
267-
if self.is_searching {
268-
if key_char == '󰁮' {
269-
if self.search_prefix.is_empty() {
270-
self.is_searching = false;
271-
self.set_mode(mode.to_app_mode());
272-
if self.word_picker.is_some() {
273-
self.draw_word_picker();
274-
} else {
275-
self.drawer.clear_menus();
276-
}
277-
return;
278-
} else {
279-
self.search_prefix.pop();
280-
}
281-
} else {
282-
self.search_prefix.push(key_char.to_ascii_lowercase());
283-
}
284-
self.drawer.draw_search_bar(&self.search_prefix, false);
285-
} else if key_char == '󰁮' {
273+
pub(super) async fn filter_by_hint(&mut self, key_char: char, mode: FilterMode) {
274+
if key_char == '󰁮' {
286275
if self.hint_prefix.is_empty() {
287276
self.go_back_in_filtering(mode);
288277
return;
@@ -293,10 +282,45 @@ impl AppEngine {
293282
self.hint_prefix.push(key_char);
294283
}
295284

296-
// TODO: debounce in searching mode?
297285
self.check_filtering(mode).await;
298286
}
299287

288+
pub(super) async fn filter_by_search(&mut self, key_char: char, mode: FilterMode) {
289+
if key_char == '󰁮' {
290+
if self.search_prefix.is_empty() {
291+
self.is_searching = false;
292+
self.set_mode(mode.to_app_mode());
293+
if self.word_picker.is_some() {
294+
self.drawer.hide_search_bar();
295+
self.draw_word_picker();
296+
} else {
297+
self.drawer.clear_menus();
298+
self.update_hints();
299+
}
300+
return;
301+
} else {
302+
self.search_prefix.pop();
303+
}
304+
} else {
305+
self.search_prefix.push(key_char.to_ascii_lowercase());
306+
}
307+
308+
self.drawer.draw_search_bar(&self.search_prefix, false);
309+
310+
// Delay the heavy `check_filtering` call
311+
self.search_debounce_counter = self.search_debounce_counter.wrapping_add(1);
312+
let current_id = self.search_debounce_counter;
313+
let sender = self.signal_sender.clone();
314+
tokio::spawn(async move {
315+
delay(
316+
sender,
317+
AppSignal::SearchDebounce(current_id, mode),
318+
DEBOUNCE_TIMEOUT,
319+
)
320+
.await
321+
});
322+
}
323+
300324
fn ready_for_unique(&self) -> bool {
301325
!self.is_searching
302326
&& (self.hint_prefix.len() == self.hint_width as usize

src/app_engine/lifecycle.rs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use super::AppEngine;
22
use crate::{
3-
Mode,
3+
AppSignal, Mode,
44
ax_element::{ElementOfInterest, ElementSignal, Target, ThreadSafeElement, traverse},
55
config::{GlyphlowConfig, RoleOfInterest, VisibilityCheckingLevel},
66
os_util::get_focused_window,
@@ -55,6 +55,7 @@ impl AppEngine {
5555
self.search_prefix.clear();
5656
self.is_searching = false;
5757
self.multi_selection.reset();
58+
self.search_debounce_counter = self.search_debounce_counter.wrapping_add(1);
5859
}
5960

6061
pub(super) fn notify_then_deactivate(&mut self, msg: &str, log_level: Level) {
@@ -67,11 +68,13 @@ impl AppEngine {
6768
Level::Trace | Level::Info => SHORT_TIMEOUT,
6869
Level::Debug => DEBUG_TIMEOUT,
6970
_ => LONG_TIMEOUT,
70-
};
71+
} * 1000;
7172
log::log!(log_level, "{msg}");
7273
let id = self.drawer.notify(msg);
73-
let sender = self.timeout_sender.clone();
74-
tokio::spawn(async move { delay(sender, id, timeout_secs).await });
74+
let sender = self.signal_sender.clone();
75+
tokio::spawn(
76+
async move { delay(sender, AppSignal::ClearNotification(id), timeout_secs).await },
77+
);
7578
}
7679

7780
pub(super) fn get_app_window_info(&mut self) {
@@ -238,7 +241,9 @@ impl AppEngine {
238241
self.notify("Press Enter to act.", Level::Trace);
239242
}
240243
// For internal activations like workflow action / element explorer
241-
self.set_mode(Mode::Filtering);
244+
if matches!(target, Target::ChildElement | Target::Custom(_)) {
245+
self.set_mode(Mode::Filtering);
246+
}
242247
} else if self.target == Target::Scrollable
243248
&& let Some(eoi) = self.selected.as_ref()
244249
{
@@ -282,7 +287,7 @@ impl AppEngine {
282287
}
283288
}
284289

285-
async fn delay(sender: Sender<usize>, id: usize, timeout_secs: u64) {
286-
tokio::time::sleep(Duration::from_secs(timeout_secs)).await;
290+
pub async fn delay<T>(sender: Sender<T>, id: T, timeout_millis: u64) {
291+
tokio::time::sleep(Duration::from_millis(timeout_millis)).await;
287292
let _ = sender.send(id).await;
288293
}

src/app_engine/mod.rs

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,8 @@ pub struct AppEngine {
8282
pub(super) editing: Option<ElementOfInterest>,
8383
/// For editing element text values
8484
pub(super) temp_file: PathBuf,
85-
pub(super) timeout_sender: Sender<usize>,
85+
pub(super) signal_sender: Sender<AppSignal>,
86+
pub(super) search_debounce_counter: usize,
8687
/// Special treatment for Electron based apps.
8788
/// Like simulate mouse clicking instead of `element.press()`
8889
pub(super) last_app_window_info: AppWindowInfo,
@@ -98,7 +99,7 @@ impl AppEngine {
9899
key_state: Arc<Mutex<KeyState>>,
99100
config: GlyphlowConfig,
100101
temp_file: PathBuf,
101-
timeout_sender: Sender<usize>,
102+
signal_sender: Sender<AppSignal>,
102103
) -> Self {
103104
let mtm = MainThreadMarker::new().expect("Not on main thread");
104105
let screen_frames = get_screen_frames(mtm);
@@ -125,7 +126,8 @@ impl AppEngine {
125126
overlay_frame,
126127
drawer,
127128
config,
128-
timeout_sender,
129+
signal_sender,
130+
search_debounce_counter: 0,
129131
selected: None,
130132
editing: None,
131133
temp_file,
@@ -149,13 +151,17 @@ impl AppEngine {
149151
AppSignal::DeActivate => {
150152
self.deactivate();
151153
}
154+
AppSignal::MenuRefresh(key_prefix) => {
155+
self.menu_refresh(&key_prefix, false);
156+
}
152157
AppSignal::RunWorkFlow(idx) => {
153158
self.drawer.clear_menus();
154159
self.execute_workflow(idx);
155160
}
156-
AppSignal::MenuRefresh(key_prefix) => {
157-
self.menu_refresh(&key_prefix, false);
161+
AppSignal::ScrollAction(sa) => {
162+
self.perform_scroll_action(sa);
158163
}
164+
AppSignal::TextAction(ta) => self.perform_text_action(ta),
159165
AppSignal::ToggleMultiSelection => match self.target {
160166
Target::Text | Target::ImageOCR => {
161167
self.toggle_multiselection();
@@ -167,29 +173,36 @@ impl AppEngine {
167173
self.notify("Multi selection only works for text.", Level::Warn);
168174
}
169175
},
170-
AppSignal::Filter(key_char, mode) => {
171-
self.perform_filtering(key_char, mode).await;
176+
AppSignal::HintFilter(key_char, mode) => {
177+
self.filter_by_hint(key_char, mode).await;
172178
}
173-
AppSignal::ScrollAction(sa) => {
174-
self.perform_scroll_action(sa);
179+
AppSignal::SearchFilter(key_char, mode) => {
180+
self.filter_by_search(key_char, mode).await;
181+
}
182+
AppSignal::SearchDebounce(id, mode) => {
183+
if self.is_searching && id == self.search_debounce_counter {
184+
self.check_filtering(mode).await;
185+
}
175186
}
176-
AppSignal::TextAction(ta) => self.perform_text_action(ta),
177187
AppSignal::StartSearch => {
178188
if self.is_searching {
179189
return;
180190
}
181191
self.is_searching = true;
192+
self.drawer.draw_search_bar(&self.search_prefix, true);
182193
if self.word_picker.is_some() {
183194
self.draw_word_picker();
184195
} else {
185-
self.drawer.draw_search_bar(&self.search_prefix, true);
186196
self.build_search_targets();
187197
}
188198
}
189199
AppSignal::FinishSearch(mode) => {
190200
self.is_searching = false;
201+
self.search_debounce_counter = self.search_debounce_counter.wrapping_add(1);
191202
self.set_mode(mode.to_app_mode());
192-
if self.word_picker.is_none() {
203+
if self.word_picker.is_some() {
204+
self.drawer.hide_search_bar();
205+
} else {
193206
self.drawer.clear_menus();
194207
}
195208
self.check_filtering(mode).await;

src/key_listener.rs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ pub enum AppSignal {
5555
// State signals
5656
Activate(Target),
5757
DeActivate,
58-
Filter(char, FilterMode),
58+
HintFilter(char, FilterMode),
59+
SearchFilter(char, FilterMode),
5960
MenuRefresh(String),
6061
ActOnEnter,
6162
// Sub state signals
@@ -72,6 +73,7 @@ pub enum AppSignal {
7273
FrameOCR,
7374
StartSearch,
7475
FinishSearch(FilterMode),
76+
SearchDebounce(usize, FilterMode),
7577
}
7678

7779
#[derive(Debug, PartialEq)]
@@ -344,7 +346,7 @@ impl KeyListener {
344346
self.send(AppSignal::DeActivate);
345347
*state = Mode::Idle;
346348
} else {
347-
self.send(AppSignal::Filter(key_char, mode));
349+
self.send(AppSignal::HintFilter(key_char, mode));
348350
}
349351
}
350352
}
@@ -398,7 +400,7 @@ impl KeyListener {
398400
key.to_char()
399401
};
400402
let key_char = if key_char == ' ' { '󱁐' } else { key_char };
401-
self.send(AppSignal::Filter(key_char, mode));
403+
self.send(AppSignal::SearchFilter(key_char, mode));
402404
}
403405
};
404406
true

0 commit comments

Comments
 (0)