Skip to content

Commit f55e6ee

Browse files
authored
fix: minor issues (#56)
* refactor: clear_cache * chore: disable search bar relocation animation * refactor * refactor: no more padding for `format_fixed_width`
1 parent 66bf6f4 commit f55e6ee

7 files changed

Lines changed: 84 additions & 107 deletions

File tree

src/app_engine/interaction.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::AppEngine;
22
use crate::{
33
Mode, ScrollAction, TextAction,
4-
action::{WordPicker, get_dictionary_attributed_string},
4+
action::{WordPicker, get_dictionary_attributed_string, text_to_clipboard},
55
ax_element::{ElementOfInterest, GetAttribute, SetAttribute},
66
config::RoleOfInterest,
77
};
@@ -143,7 +143,7 @@ impl AppEngine {
143143
// 1. URL handling
144144
let keep_drawing = match ta {
145145
TextAction::Copy => {
146-
crate::action::text_to_clipboard(&text);
146+
text_to_clipboard(&text);
147147
self.notify_then_deactivate("Copied to clipboard.", Level::Info);
148148
true
149149
}

src/app_engine/lifecycle.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,10 @@ impl AppEngine {
5353
self.element_cache.clear();
5454
self.hint_prefix.clear();
5555
self.search_prefix.clear();
56+
self.search_targets.clear();
57+
self.search_debounce_counter = 0;
5658
self.is_searching = false;
5759
self.multi_selection.reset();
58-
self.search_debounce_counter = self.search_debounce_counter.wrapping_add(1);
5960
}
6061

6162
pub(super) fn notify_then_deactivate(&mut self, msg: &str, log_level: Level) {

src/app_engine/mod.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ pub struct AppEngine {
6767
/// Used for drawing hint boxes on screen
6868
pub(super) hint_boxes: Vec<HintBox>,
6969
pub(super) hint_prefix: String,
70+
/// Search related
7071
pub(super) is_searching: bool,
7172
pub(super) search_prefix: String,
7273
pub(super) search_targets: Vec<String>,
74+
pub(super) search_debounce_counter: usize,
7375
pub(super) overlay_frame: Frame,
7476
pub(super) drawer: UIDrawer,
7577
/// Which elements of interest to look for
@@ -83,7 +85,6 @@ pub struct AppEngine {
8385
/// For editing element text values
8486
pub(super) temp_file: PathBuf,
8587
pub(super) signal_sender: Sender<AppSignal>,
86-
pub(super) search_debounce_counter: usize,
8788
/// Special treatment for Electron based apps.
8889
/// Like simulate mouse clicking instead of `element.press()`
8990
pub(super) last_app_window_info: AppWindowInfo,
@@ -198,7 +199,7 @@ impl AppEngine {
198199
}
199200
AppSignal::FinishSearch(mode) => {
200201
self.is_searching = false;
201-
self.search_debounce_counter = self.search_debounce_counter.wrapping_add(1);
202+
self.search_debounce_counter = 0;
202203
self.set_mode(mode.to_app_mode());
203204
if self.word_picker.is_some() {
204205
self.drawer.hide_search_bar();

src/ax_element.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -673,7 +673,7 @@ fn traverse_elements(
673673
return;
674674
};
675675

676-
// WARN: Performance critical! Exclude electron elements scrolled off y axis,
676+
// PERF: Performance critical! Exclude electron elements scrolled off y axis,
677677
if ele_fp.frame.is_some_and(|f| {
678678
let (w, h) = f.size();
679679
(h == 0.0 && f.bottom_right.y == window_frame.bottom_right.y)

src/user_interface/drawer.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,7 +298,11 @@ impl UIDrawer {
298298
let msg = format!("/{}", format_fixed_width(prefix, SEARCH_BAR_WIDTH));
299299
autoreleasepool(|_| {
300300
if init {
301+
// Disable movement animations
302+
CATransaction::begin();
303+
CATransaction::setDisableActions(true);
301304
self.reposition_search_bar();
305+
CATransaction::commit();
302306
self.search_bar.show();
303307
}
304308

src/user_interface/hint_box.rs

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,12 @@
1-
use std::collections::{HashMap, VecDeque};
2-
1+
use crate::config::GlyphlowTheme;
2+
use crate::user_interface::calibrated_origin;
3+
use crate::util::{Frame, digits_by_length, estimate_frame_for_text};
34
use objc2::{AnyThread, rc::Retained};
45
use objc2_core_foundation::{CFRetained, CGSize};
56
use objc2_core_graphics::{CGColor, CGMutablePath};
67
use objc2_foundation::{NSMutableAttributedString, NSPoint, NSRange, NSRect, NSSize, NSString};
78
use objc2_quartz_core::{CALayer, CAShapeLayer, CATextLayer, kCAAlignmentCenter};
8-
9-
use crate::config::GlyphlowTheme;
10-
use crate::user_interface::calibrated_origin;
11-
use crate::util::{Frame, digits_by_length, estimate_frame_for_text};
9+
use std::collections::{HashMap, VecDeque};
1210

1311
pub fn hint_label_from_index(i: usize, digits: Option<u32>) -> String {
1412
if i == 0 && digits.is_none() {

src/util.rs

Lines changed: 68 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
use std::cmp::Ordering;
2-
3-
use core_foundation::attributed_string::CFAttributedStringRef;
1+
use core_foundation::{attributed_string::CFAttributedStringRef, base::CFRange};
2+
use core_graphics_types::geometry::CGSize;
43
use core_text::framesetter::CTFramesetter;
54
use objc2::rc::Retained;
65
use objc2_core_foundation::{CGPoint, CGRect, CGSize as OCGSize};
76
use objc2_foundation::{NSMutableAttributedString, NSSize};
87
use regex::Regex;
8+
use std::{borrow::Cow, cmp::Ordering};
99
use unicode_width::{UnicodeWidthChar, UnicodeWidthStr};
1010

1111
#[derive(Debug, Clone, PartialEq, Copy, Default)]
@@ -20,15 +20,11 @@ pub fn estimate_frame_for_text(
2020
) -> (OCGSize, isize) {
2121
let cf_attr_string = Retained::as_ptr(attr_string) as CFAttributedStringRef;
2222
let framesetter = CTFramesetter::new_with_attributed_string(cf_attr_string);
23-
let (core_graphics_types::geometry::CGSize { width, height }, range) = framesetter
24-
.suggest_frame_size_with_constraints(
25-
core_foundation::base::CFRange {
26-
location: 0,
27-
length: 0,
28-
},
29-
std::ptr::null(),
30-
core_graphics_types::geometry::CGSize::new(size.0, size.1),
31-
);
23+
let (CGSize { width, height }, range) = framesetter.suggest_frame_size_with_constraints(
24+
CFRange::init(0, 0),
25+
std::ptr::null(),
26+
CGSize::new(size.0, size.1),
27+
);
3228
(OCGSize::new(width, height), range.length)
3329
}
3430

@@ -290,49 +286,30 @@ pub fn lower_ascii(text: &str) -> String {
290286
any_ascii::any_ascii(text).to_ascii_lowercase()
291287
}
292288

293-
pub fn format_fixed_width(input: &str, fixed_length: usize) -> String {
289+
pub fn format_fixed_width(input: &str, fixed_length: usize) -> Cow<'_, str> {
290+
if input.width() <= fixed_length {
291+
return Cow::Borrowed(input);
292+
}
293+
294294
if fixed_length == 0 {
295295
return "".into();
296296
}
297-
let target_suffix_w = fixed_length - 1;
297+
298298
let mut current_width = 0;
299299
let mut trunc_byte_idx = input.len();
300-
let mut trunc_width = 0;
301300

302-
// Scan backwards: Only inspects up to `fixed_length + 1` worth of characters
303-
for (idx, ch) in input.char_indices().rev() {
304-
let ch_width = ch.width().unwrap_or(1);
301+
for ch in input.chars().rev() {
302+
let ch_width = ch.width().unwrap_or_default();
305303
current_width += ch_width;
306-
307-
// Track the best byte cut-off point for the suffix
308-
if current_width <= target_suffix_w {
309-
trunc_byte_idx = idx;
310-
trunc_width = current_width;
311-
}
312-
313-
// Early exit: The moment we know the string is too long, we build the truncation
314-
if current_width > fixed_length {
315-
let suffix = &input[trunc_byte_idx..];
316-
let padding_spaces = target_suffix_w - trunc_width;
317-
318-
let mut s = String::with_capacity(1 + padding_spaces + suffix.len());
319-
s.push('.');
320-
for _ in 0..padding_spaces {
321-
s.push(' ');
322-
}
323-
s.push_str(suffix);
324-
return s;
304+
if current_width > fixed_length - 1 {
305+
break;
325306
}
307+
trunc_byte_idx -= ch.len_utf8();
326308
}
327309

328-
// Case: The entire string fits within the layout -> Pad it out
329-
let padding_spaces = fixed_length - current_width;
330-
let mut s = String::with_capacity(input.len() + padding_spaces);
331-
s.push_str(input);
332-
for _ in 0..padding_spaces {
333-
s.push(' ');
334-
}
335-
s
310+
let mut s = ".".to_string();
311+
s.push_str(&input[trunc_byte_idx..]);
312+
Cow::Owned(s)
336313
}
337314

338315
#[cfg(test)]
@@ -683,78 +660,74 @@ mod select_range_tests {
683660
}
684661

685662
#[cfg(test)]
686-
mod misc_tests {
663+
mod format_str_tests {
687664
use super::format_fixed_width;
688665

689666
#[test]
690-
fn test_exact_and_padding_ascii() {
691-
// Exact match: No padding, no truncation
692-
assert_eq!(format_fixed_width("hello", 5), "hello");
667+
fn test_empty_and_zero_edges() {
668+
// Empty input should always result in an empty string
669+
assert_eq!(format_fixed_width("", 5).to_string(), "");
670+
assert_eq!(format_fixed_width("", 0).to_string(), "");
693671

694-
// Needs padding: Short string, padded to the right with spaces
695-
assert_eq!(format_fixed_width("abc", 6), "abc ");
672+
// Fixed length of 0 on non-empty input should return an empty string
673+
assert_eq!(format_fixed_width("hello", 0).to_string(), "");
696674
}
697675

698676
#[test]
699-
fn test_truncation_ascii() {
700-
// Standard truncation from the left with a single dot
701-
// "abcdefghijk" length is 11. Target is 10.
702-
// 10 - 1 (for ".") = 9 slots for the suffix ("cdefghijk")
703-
assert_eq!(format_fixed_width("abcdefghijk", 10), ".cdefghijk");
704-
705-
// Tight truncation
706-
assert_eq!(format_fixed_width("abcdef", 4), ".def");
677+
fn test_ascii_no_truncation() {
678+
assert_eq!(format_fixed_width("hello", 5).to_string(), "hello");
679+
assert_eq!(format_fixed_width("abc", 5).to_string(), "abc");
707680
}
708681

709682
#[test]
710-
fn test_empty_and_minimal_lengths() {
711-
// Empty string should just turn into pure padding
712-
assert_eq!(format_fixed_width("", 5), " ");
713-
assert_eq!(format_fixed_width("", 0), "");
683+
fn test_ascii_truncation() {
684+
// Budget = 3. Suffix width = 3 - 1 = 2 ("lo")
685+
assert_eq!(format_fixed_width("hello", 3).to_string(), ".lo");
714686

715-
// Extreme edge case: Requested length is exactly 1 or 2
716-
assert_eq!(format_fixed_width("longstring", 2), ".g");
717-
assert_eq!(format_fixed_width("longstring", 1), ".");
687+
// Budget = 1. Suffix width = 1 - 1 = 0 ("")
688+
assert_eq!(format_fixed_width("hello", 1).to_string(), ".");
718689
}
719690

720691
#[test]
721-
fn test_unicode_padding() {
722-
// "こんにちは" (Konnichiwa) has 5 characters, but a visual width of 10.
723-
// Target 12 means it needs exactly 2 trailing spaces.
724-
assert_eq!(format_fixed_width("こんにちは", 12), "こんにちは ");
692+
fn test_cjk_characters() {
693+
// "こんにちは" (Konnichiwa) has a total visual width of 10
694+
let input = "こんにちは";
695+
696+
assert_eq!(format_fixed_width(input, 10).to_string(), "こんにちは");
697+
698+
// Exact fit truncation: Dot (1) + "に" (2) + "は" (2) = 5
699+
assert_eq!(format_fixed_width(input, 5).to_string(), ".ちは");
700+
701+
// Truncation with 1 unit of slack space (next character doesn't fit)
702+
assert_eq!(format_fixed_width(input, 6).to_string(), ".ちは");
725703
}
726704

727705
#[test]
728-
fn test_unicode_clean_truncation() {
729-
// Target 7 -> Suffix target is 6 (7 - 1).
730-
// "にちは" is 3 characters of width 2 each = 6 total width. Fits perfectly.
731-
assert_eq!(format_fixed_width("こんにちは", 7), ".にちは");
706+
fn test_emojis() {
707+
// "🦀" has a visual width of 2. "🦀🦀🦀" total width = 6.
708+
let input = "🦀🦀🦀";
709+
710+
assert_eq!(format_fixed_width(input, 6).to_string(), "🦀🦀🦀");
711+
712+
// Truncation: Dot (1) + "🦀" (2) + "🦀" (2) = 5
713+
assert_eq!(format_fixed_width(input, 5).to_string(), ".🦀🦀");
732714
}
733715

734716
#[test]
735-
fn test_unicode_mid_character_truncation_alignment() {
736-
// CRITICAL EDGE CASE: Truncation hits right in the middle of a wide character.
737-
// Target 8 -> Suffix target is 7 (8 - 1).
738-
// "にちは" has a width of 6. The next char "ん" has a width of 2 (Total 8).
739-
// 8 exceeds our suffix target of 7, so "ん" must be dropped.
740-
// This leaves 1 empty visual slot (7 - 6 = 1), which must be filled with a space.
741-
assert_eq!(format_fixed_width("こんにちは", 8), ". にちは");
742-
743-
// Verify total visual width of the output is exactly 8
744-
// "." (1) + " " (1) + "に" (2) + "ち" (2) + "は" (2) = 8
745-
let result = format_fixed_width("こんにちは", 8);
746-
let actual_width: usize = result
747-
.chars()
748-
.map(|c| unicode_width::UnicodeWidthChar::width(c).unwrap_or(1))
749-
.sum();
750-
assert_eq!(actual_width, 8);
717+
fn test_mixed_width_strings() {
718+
// "Rust🦀" -> "Rust" (4) + "🦀" (2) = Total width 6
719+
let input = "Rust🦀";
720+
721+
assert_eq!(format_fixed_width(input, 3).to_string(), ".🦀");
722+
assert_eq!(format_fixed_width(input, 5).to_string(), ".st🦀");
751723
}
752724

753725
#[test]
754-
fn test_fallback_width_handling() {
755-
// Control characters like '\n' return None from .width()
756-
// Your updated logic defaults them to 1 via .unwrap_or(1)
757-
// "\n\n" is treated as width 2. Target 5 -> pads with 3 spaces.
758-
assert_eq!(format_fixed_width("\n\n", 5), "\n\n ");
726+
fn test_combining_diacritic_quirk() {
727+
// "xyz" + combining acute accent (\u{301})
728+
let input = "xyz\u{301}";
729+
730+
// Accent detaches and glues itself to the ellipsis dot
731+
assert_eq!(format_fixed_width(input, 1).to_string(), ".\u{301}");
759732
}
760733
}

0 commit comments

Comments
 (0)