diff --git a/src-tauri/src/utils/tray_speed.rs b/src-tauri/src/utils/tray_speed.rs index 8223ea3fc..c3bda426e 100644 --- a/src-tauri/src/utils/tray_speed.rs +++ b/src-tauri/src/utils/tray_speed.rs @@ -27,8 +27,12 @@ const TRAY_STATUS_ITEM_EXTRA_WIDTH: f64 = 30.0; const TRAY_STATUS_ITEM_MIN_LENGTH: f64 = 58.0; const NS_VARIABLE_STATUS_ITEM_LENGTH: f64 = -1.0; +/// (button_height, mono_advance, attributes). +type CachedAttrs = (f64, f64, Retained>); + thread_local! { static LAST_DISPLAY_STR: RefCell = const { RefCell::new(String::new()) }; + static CACHED_ATTRS: RefCell> = const { RefCell::new(None) }; } fn format_tray_speed(up: u64, down: u64) -> String { @@ -74,8 +78,26 @@ fn create_attributed_string( } } -fn status_item_length_for_speed(attr_str: &NSAttributedString) -> f64 { - (attr_str.size().width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH) +/// Attributes and per-glyph advance, rebuilt only when the button height changes. +fn cached_attributes(button_height: f64) -> (f64, Retained>) { + CACHED_ATTRS.with(|cache| { + if let Some((height, advance, attrs)) = cache.borrow().as_ref() + && *height == button_height + { + return (*advance, attrs.clone()); + } + let attrs = build_attributes(button_height); + let probe = NSString::from_str("0"); + let advance = create_attributed_string(&probe, Some(&attrs)).size().width; + *cache.borrow_mut() = Some((button_height, advance, attrs.clone())); + (advance, attrs) + }) +} + +/// Longest line times the monospaced advance; matches `size().width` for the ASCII-only speed text. +fn arithmetic_width(text: &str, advance: f64) -> f64 { + let glyphs = text.split('\n').map(str::len).max().unwrap_or(0) as f64; + glyphs * advance } fn sync_click_target_frame(button: &NSStatusBarButton) { @@ -89,7 +111,7 @@ fn sync_click_target_frame(button: &NSStatusBarButton) { } /// `with_inner_tray_icon` should run this on the main thread; fail safely if it does not. -fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSString, show_speed: bool) { +fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &str, show_speed: bool) { let Some(mtm) = MainThreadMarker::new() else { logging!(warn, Type::Tray, "托盘速率富文本设置跳过:非主线程调用"); return; @@ -97,17 +119,19 @@ fn apply_status_item_attributed_title(status_item: &NSStatusItem, text: &NSStrin let Some(button) = status_item.button(mtm) else { return; }; + let ns_string = NSString::from_str(text); let attr_str = if show_speed { - let attrs = build_attributes(button.bounds().size.height); + let (advance, attrs) = cached_attributes(button.bounds().size.height); let attrs: &NSDictionary = &attrs; - let attr_str = create_attributed_string(text, Some(attrs)); - let length = status_item_length_for_speed(&attr_str); + let attr_str = create_attributed_string(&ns_string, Some(attrs)); + let width = arithmetic_width(text, advance); + let length = (width.ceil() + TRAY_STATUS_ITEM_EXTRA_WIDTH).max(TRAY_STATUS_ITEM_MIN_LENGTH); if status_item.length() != length { status_item.setLength(length); } attr_str } else { - create_attributed_string(text, None) + create_attributed_string(&ns_string, None) }; button.setAttributedTitle(&attr_str); sync_click_target_frame(&button); @@ -121,8 +145,7 @@ pub fn set_speed_attributed_title(status_item: &NSStatusItem, up: u64, down: u64 return; } *last = speed_text; - let ns_string = NSString::from_str(last.as_str()); - apply_status_item_attributed_title(status_item, &ns_string, true); + apply_status_item_attributed_title(status_item, last.as_str(), true); }); } @@ -130,7 +153,6 @@ pub fn clear_speed_attributed_title(status_item: &NSStatusItem) { LAST_DISPLAY_STR.with(|last| { last.borrow_mut().clear(); }); - let empty = NSString::from_str(""); status_item.setLength(NS_VARIABLE_STATUS_ITEM_LENGTH); - apply_status_item_attributed_title(status_item, &empty, false); + apply_status_item_attributed_title(status_item, "", false); }