From 5f6fde9144ea724287fd75f311c42eda0c4d1bf0 Mon Sep 17 00:00:00 2001 From: fylorn <249551762+fylorn@users.noreply.github.com> Date: Sat, 17 Oct 2026 12:54:36 +0800 Subject: [PATCH] Menu bar: a sweeping glow on the mark replaces the in-flight dot While requests are running, the mark (or the numbers, in the numbers-only style) dims and a bright band sweeps across it: 1.3 s across, 0.5 s rest. It is a Core Animation mask on the status button's layer, so the render server drives it and the app does no per-frame work (about 0.4% CPU in a debug build, against about 4% for redrawing the button 20 times a second). The mask is rebuilt only when the button or image geometry changes, and the animation keeps a fixed begin time so the band does not restart. It is taken off while the menu is open (older macOS draws the highlight in the button) and stands still in the middle when Reduce Motion is on. bar_image takes an optional phase so previews and screenshots can draw a given frame; menubar_preview gains --glow to export one cycle of frames. Co-Authored-By: Claude Opus 5.5 --- src-tauri/Cargo.lock | 2 + src-tauri/Cargo.toml | 2 + src-tauri/examples/menubar_preview.rs | 20 +++ src-tauri/examples/menubar_shots.rs | 2 +- src-tauri/src/menubar/macos.rs | 239 +++++++++++++++++++++++-- src-tauri/src/menubar/macos/preview.rs | 7 +- src-tauri/src/menubar/model.rs | 6 +- src-tauri/src/menubar/model/tests.rs | 6 +- 8 files changed, 257 insertions(+), 27 deletions(-) diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index fe443e82..ee1804db 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -2883,6 +2883,7 @@ dependencies = [ "bitflags 2.13.2", "objc2", "objc2-core-foundation", + "objc2-core-graphics", "objc2-foundation", ] @@ -4847,6 +4848,7 @@ dependencies = [ "objc2", "objc2-app-kit", "objc2-foundation", + "objc2-quartz-core", "objc2-user-notifications", "rand 0.10.3", "reqwest", diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 0a269a20..ac27b066 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -131,6 +131,8 @@ objc2-foundation = "0.3" objc2-user-notifications = "0.3" # 菜单栏自己持有 NSStatusItem 和 NSMenu(见 `menubar/macos.rs`)。和 Tauri 用的是同一版 objc2-app-kit = "0.3" +# 菜单栏上的流光是图层遮罩上的动画(见 `menubar/macos.rs` 的 `glow`) +objc2-quartz-core = { version = "0.3", default-features = false, features = ["std", "objc2-core-graphics", "CABase", "CALayer", "CAAnimation", "CAMediaTiming", "CATransaction"] } # 菜单开着时主线程在事件跟踪模式,更新要走 GCD 的主队列才执行得到 dispatch2 = "0.3" # 更新包是 DMG,而更新器插件只装 `.app.tar.gz`:挂上映像,把里面的 `.app` diff --git a/src-tauri/examples/menubar_preview.rs b/src-tauri/examples/menubar_preview.rs index b062c097..75a7cedd 100644 --- a/src-tauri/examples/menubar_preview.rs +++ b/src-tauri/examples/menubar_preview.rs @@ -5,6 +5,8 @@ //! cargo run --manifest-path src-tauri/Cargo.toml --example menubar_preview -- --png <目录> //! # 再加上菜单栏那一块的三档 //! cargo run --manifest-path src-tauri/Cargo.toml --example menubar_preview -- --png <目录> --bars +//! # 只导出有请求在跑时的流光,一圈 54 帧(30 帧/秒) +//! cargo run --manifest-path src-tauri/Cargo.toml --example menubar_preview -- --png <目录> --glow //! # 挂到菜单栏上,点开看真的菜单。Ctrl-C 退出 //! cargo run --manifest-path src-tauri/Cargo.toml --example menubar_preview -- busy //! ``` @@ -62,6 +64,24 @@ fn main() { } } } + if args.iter().any(|a| a == "--glow") { + // 有请求在跑时菜单栏那一块的流光,一圈按 30 帧/秒导出成一串 + let bar = model::build(&snapshot("busy"), style).0; + for i in 0..54 { + let phase = f64::from(i) / 30.0 / 1.3; + for dark in [true, false] { + let path = dir.join(format!( + "glow-{}-{i:02}.png", + if dark { "dark" } else { "light" } + )); + macos::preview::write_png( + &macos::preview::preview_frame(&bar, &[], dark, phase), + &path, + ); + } + } + return; + } if !args.iter().any(|a| a == "--bars") { return; } diff --git a/src-tauri/examples/menubar_shots.rs b/src-tauri/examples/menubar_shots.rs index ac871270..00a922cc 100644 --- a/src-tauri/examples/menubar_shots.rs +++ b/src-tauri/examples/menubar_shots.rs @@ -187,7 +187,7 @@ fn main() { // 菜单栏上的那一块:一段菜单栏底色,模板图照系统的样子染成黑或白 let (bar, _) = model::build(&snapshot(), model::Style::Full); for dark in [false, true] { - let item = macos::bar_image(&bar); + let item = macos::bar_image(&bar, Some(0.5)); let size = item.size(); let (w, h) = ((size.width + 28.0).ceil(), 32.0); let block = RcBlock::new(move |_: NSRect| -> Bool { diff --git a/src-tauri/src/menubar/macos.rs b/src-tauri/src/menubar/macos.rs index 58c59016..1733f385 100644 --- a/src-tauri/src/menubar/macos.rs +++ b/src-tauri/src/menubar/macos.rs @@ -25,14 +25,19 @@ use objc2::{AnyThread, MainThreadMarker, MainThreadOnly, define_class, msg_send, use objc2_app_kit::{ NSAlert, NSAlertFirstButtonReturn, NSAlertSecondButtonReturn, NSAlertStyle, NSAppearanceCustomization, NSApplication, NSAttributedStringNSStringDrawing, NSBezierPath, - NSColor, NSControlStateValueOff, NSControlStateValueOn, NSEvent, NSEventMask, - NSEventModifierFlags, NSFont, NSFontAttributeName, NSFontWeightMedium, NSFontWeightRegular, - NSFontWeightSemibold, NSForegroundColorAttributeName, NSImage, NSImageSymbolConfiguration, - NSLineCapStyle, NSLineJoinStyle, NSMenu, NSMenuDelegate, NSMenuItem, NSStatusBar, NSStatusItem, - NSVariableStatusItemLength, + NSColor, NSCompositingOperation, NSControlStateValueOff, NSControlStateValueOn, NSEvent, + NSEventMask, NSEventModifierFlags, NSFont, NSFontAttributeName, NSFontWeightMedium, + NSFontWeightRegular, NSFontWeightSemibold, NSForegroundColorAttributeName, NSGradient, + NSGraphicsContext, NSImage, NSImageSymbolConfiguration, NSLineCapStyle, NSLineJoinStyle, + NSMenu, NSMenuDelegate, NSMenuItem, NSStatusBar, NSStatusItem, NSVariableStatusItemLength, + NSWorkspace, }; use objc2_foundation::{ - NSAttributedString, NSDictionary, NSObject, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString, + NSArray, NSAttributedString, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, + NSRect, NSSize, NSString, +}; +use objc2_quartz_core::{ + CAAnimationGroup, CABasicAnimation, CACurrentMediaTime, CALayer, CAMediaTiming, CATransaction, }; use super::model::{Action, Bar, Row, Style, SubItem}; @@ -78,6 +83,8 @@ struct Ui { /// 按菜单项的 tag 找到它做什么 actions: Vec, bar: Option, + /// 挂着的流光遮罩(见 [`glow`])。没挂是 None + glow: Option, } thread_local! { @@ -114,6 +121,7 @@ pub fn install( rows: Vec::new(), actions: Vec::new(), bar: None, + glow: None, }); }); } @@ -133,6 +141,7 @@ pub fn apply(mtm: MainThreadMarker, bar: &Bar, rows: &[Row]) { draw_bar(mtm, item, bar); ui.bar = Some(bar.clone()); } + glow(mtm, ui); let shapes: Vec = rows.iter().map(Row::shape).collect(); if shapes == ui.shapes { refresh(mtm, ui, rows); @@ -146,7 +155,7 @@ pub fn apply(mtm: MainThreadMarker, bar: &Bar, rows: &[Row]) { /// 拿掉菜单栏上的那一项(设置里选了不显示)。**菜单留着**:换回来时挂到新建的那一项上, /// 菜单行照旧就地改。已经拿掉了就什么都不做 -pub fn hide(_mtm: MainThreadMarker) { +pub fn hide(mtm: MainThreadMarker) { UI.with(|cell| { let mut guard = cell.borrow_mut(); let Some(ui) = guard.as_mut() else { return }; @@ -156,6 +165,7 @@ pub fn hide(_mtm: MainThreadMarker) { ui.menu.cancelTrackingWithoutAnimation(); } NSStatusBar::systemStatusBar().removeStatusItem(&item); + glow(mtm, ui); // 再挂出来的是新的一项,那一块要整个重画 ui.bar = None; }); @@ -181,7 +191,7 @@ fn draw_bar(mtm: MainThreadMarker, item: &NSStatusItem, bar: &Bar) { let Some(button) = item.button(mtm) else { return; }; - let image = bar_image(bar); + let image = bar_image(bar, None); button.setImage(Some(&image)); button.setToolTip(Some(&NSString::from_str(&bar.tooltip))); let label = NSString::from_str(&bar.tooltip); @@ -189,6 +199,162 @@ fn draw_bar(mtm: MainThreadMarker, item: &NSStatusItem, bar: &Bar) { let _: () = unsafe { msg_send![&*button, setAccessibilityLabel: &*label] }; } +/// 流光走一圈多久:[`SWEEP`] 秒扫过去,剩下的时间停在暗处,再从左边进来 +const GLOW_CYCLE: f64 = 1.8; +const SWEEP: f64 = 1.3; + +/// 菜单栏上现在挂着的流光遮罩,和它是照什么尺寸搭的(按钮宽高、流光那一段的左右) +struct Glow { + key: [f64; 4], +} + +thread_local! { + /// 流光从哪一刻起算(Core Animation 的时钟)。遮罩换了尺寸重搭时接着这一刻走, + /// 亮带不从头来 + static GLOW_EPOCH: f64 = CACurrentMediaTime(); +} + +/// 菜单开着、关上时调:开着时流光停下(见 [`glow`]) +fn sync_glow(mtm: MainThreadMarker) { + UI.with(|cell| { + let Ok(mut guard) = cell.try_borrow_mut() else { + return; + }; + if let Some(ui) = guard.as_mut() { + glow(mtm, ui); + } + }); +} + +/// 有请求在跑时给菜单栏按钮的图层挂上流光遮罩,没有了就摘掉。**动画交给 Core +/// Animation**,在系统的渲染进程里走:每帧重画按钮的话(换图或者标脏),图层重新显示、 +/// 和系统的菜单栏场景同步一回,一秒二十帧就占好几个百分点的 CPU;挂一次遮罩之后这边 +/// 什么都不用做。 +/// +/// **菜单开着时摘掉**:旧系统上菜单栏的高亮底色是按钮自己画的,遮罩会把它也压暗。 +/// 系统设了减弱动态效果就不动,亮带停在正中间 +fn glow(mtm: MainThreadMarker, ui: &mut Ui) { + let Some(button) = ui.item.as_ref().and_then(|item| item.button(mtm)) else { + ui.glow = None; + return; + }; + let want = ui.bar.as_ref().filter(|b| b.busy && !OPEN.with(Cell::get)); + let layer = { + button.setWantsLayer(true); + button.layer() + }; + let Some(layer) = layer else { return }; + let Some(bar) = want else { + if ui.glow.take().is_some() { + unsafe { layer.setMask(None) }; + } + return; + }; + let bounds = button.bounds(); + let image = match (button.cell(), button.image()) { + (Some(cell), Some(_)) => cell.imageRectForBounds(bounds), + _ => return, + }; + let (x0, x1) = glow_span(bar, image.size.width); + let key = [ + bounds.size.width, + bounds.size.height, + image.origin.x + x0, + image.origin.x + x1, + ]; + if ui.glow.as_ref().is_some_and(|g| g.key == key) { + return; + } + CATransaction::begin(); + CATransaction::setDisableActions(true); + // 新建的图层,没有父图层,也不是别的图层的遮罩 + unsafe { layer.setMask(Some(&glow_mask(bounds, key[2], key[3]))) }; + CATransaction::commit(); + ui.glow = Some(Glow { key }); +} + +/// 流光遮罩:`x0..x1` 那一段压到 [`GLOW_BASE`]、上面一条亮带来回扫,别处原样(不透明) +fn glow_mask(bounds: NSRect, x0: f64, x1: f64) -> Retained { + let h = bounds.size.height; + let mask = CALayer::layer(); + mask.setFrame(bounds); + let opaque = NSColor::blackColor().CGColor(); + for (x, w) in [(0.0, x0), (x1, bounds.size.width - x1)] { + let side = CALayer::layer(); + side.setFrame(rect(x, 0.0, w.max(0.0), h)); + side.setBackgroundColor(Some(&opaque)); + mask.addSublayer(&side); + } + let track = CALayer::layer(); + track.setFrame(rect(x0, 0.0, x1 - x0, h)); + track.setMasksToBounds(true); + track.setBackgroundColor(Some( + &NSColor::colorWithWhite_alpha(0.0, GLOW_BASE).CGColor(), + )); + let band = CALayer::layer(); + band.setFrame(rect(-2.0 * GLOW_HALF, 0.0, 2.0 * GLOW_HALF, h)); + let image = band_image(); + let contents: &AnyObject = ℑ + unsafe { band.setContents(Some(contents)) }; + if NSWorkspace::sharedWorkspace().accessibilityDisplayShouldReduceMotion() { + band.setPosition(NSPoint::new((x1 - x0) / 2.0, h / 2.0)); + } else { + // 模型上的位置在左边外面:一趟扫完、停在暗处的那一段,亮带就在那儿 + let sweep = CABasicAnimation::animationWithKeyPath(Some(&NSString::from_str("position.x"))); + let from = NSNumber::new_f64(-GLOW_HALF); + let to = NSNumber::new_f64(x1 - x0 + GLOW_HALF); + unsafe { + sweep.setFromValue(Some(&from)); + sweep.setToValue(Some(&to)); + } + sweep.setDuration(SWEEP); + let cycle = CAAnimationGroup::animation(); + cycle.setAnimations(Some(&NSArray::from_retained_slice(&[ + Retained::into_super(Retained::into_super(sweep)), + ]))); + cycle.setDuration(GLOW_CYCLE); + cycle.setRepeatCount(f32::INFINITY); + cycle.setBeginTime(GLOW_EPOCH.with(|t| *t)); + band.addAnimation_forKey(&cycle, Some(&NSString::from_str("glow"))); + } + track.addSublayer(&band); + mask.addSublayer(&track); + mask +} + +/// 亮带:中间不透明、两边透明,余弦过渡。拉伸着贴在遮罩里,横向的过渡不怕缩放 +fn band_image() -> Retained { + let block = RcBlock::new(|r: NSRect| -> Bool { + let stops: Vec> = (0..=24) + .map(|i| { + let d = (i as f64 / 12.0 - 1.0).abs(); + NSColor::colorWithWhite_alpha(0.0, lift(d)) + }) + .collect(); + if let Some(g) = + NSGradient::initWithColors(NSGradient::alloc(), &NSArray::from_retained_slice(&stops)) + { + g.drawInRect_angle(r, 0.0); + } + Bool::YES + }); + NSImage::imageWithSize_flipped_drawingHandler(NSSize::new(2.0 * GLOW_HALF, 4.0), false, &block) +} + +/// 离亮带中心 `d`(以半宽为 1)处亮多少:中心 1,到半宽处落到 0 +fn lift(d: f64) -> f64 { + (1.0 + (d.min(1.0) * std::f64::consts::PI).cos()) / 2.0 +} + +/// 流光走在图里的哪一段:有标识就是标识,只显示数字时是两行数字 +fn glow_span(bar: &Bar, image_w: f64) -> (f64, f64) { + if bar.style == Style::Numbers { + (EDGE, image_w - EDGE) + } else { + (EDGE, EDGE + MARK_W) + } +} + /// 标识的尺寸:高 15pt,按应用图标 32 格坐标里 TW 字形所在的 22×20 那一块取宽 const MARK_H: f64 = 15.0; const MARK_W: f64 = MARK_H * 22.0 / 20.0; @@ -197,8 +363,12 @@ const EDGE: f64 = 3.0; /// 画成一张图。**正常时是模板图**:系统按菜单栏的深浅自动着色;网关不在的时候要 /// 画红色角标,只能关掉模板,用跟着外观走的系统颜色自己画。**额度不改这一块的颜色** -/// (见 `Tone`):紧张、用完都在菜单里的那一行说 -pub fn bar_image(bar: &Bar) -> Retained { +/// (见 `Tone`):紧张、用完都在菜单里的那一行说。 +/// +/// 有请求在跑时标识上走流光:整个标识压暗,一条亮带从左往右扫过(只显示数字时走在 +/// 两行数字上)。菜单栏上的流光是图层遮罩(见 [`glow`]),图本身照常画,`phase` 给 None; +/// 预览和截图没有图层,给 `phase`(扫到哪儿,0 刚进来、1 刚出去)就把那一帧画进图里 +pub fn bar_image(bar: &Bar, phase: Option) -> Retained { let show_mark = bar.style != Style::Numbers; let show_numbers = bar.style != Style::Icon; let (top, bottom) = bar @@ -218,8 +388,7 @@ pub fn bar_image(bar: &Bar) -> Retained { } else { 0.0 }; - let dot_w = if !show_mark && bar.dot { 7.0 } else { 0.0 }; - let mut width = EDGE * 2.0 + dot_w + numbers_w; + let mut width = EDGE * 2.0 + numbers_w; if show_mark { width += MARK_W; } @@ -228,6 +397,9 @@ pub fn bar_image(bar: &Bar) -> Retained { } let height = thickness(); let template = !bar.alert; + let glow = phase + .filter(|_| bar.busy) + .map(|p| (glow_span(bar, width), p)); let bar = bar.clone(); let block = RcBlock::new(move |_rect: NSRect| -> Bool { let fg = if template { @@ -243,9 +415,6 @@ pub fn bar_image(bar: &Bar) -> Retained { (height - MARK_H) / 2.0, &fg.colorWithAlphaComponent(alpha), ); - if bar.dot { - fill_oval(x + MARK_W - 1.5, (height - MARK_H) / 2.0 - 1.5, 5.0, &fg); - } if bar.alert { fill_oval( x + MARK_W - 2.0, @@ -264,10 +433,6 @@ pub fn bar_image(bar: &Bar) -> Retained { (fg.clone(), alpha) }; let color = color.colorWithAlphaComponent(alpha); - if dot_w > 0.0 { - fill_oval(x, height / 2.0 - 2.5, 5.0, &color); - x += dot_w; - } // 两行都右对齐:数字变长时向左伸,右边缘不动 let a = attributed(&top, &top_font, &color); let b = attributed(&bottom, &bottom_font, &color); @@ -277,6 +442,9 @@ pub fn bar_image(bar: &Bar) -> Retained { a.drawAtPoint(NSPoint::new(right - a.size().width, y0)); b.drawAtPoint(NSPoint::new(right - b.size().width, y0 + line)); } + if let Some(((x0, x1), phase)) = glow { + glow_paint(x0, x1, height, phase); + } Bool::YES }); let image = @@ -306,6 +474,37 @@ fn draw_mark(x: f64, y: f64, color: &NSColor) { path.stroke(); } +/// 流光:暗处的透明度,和亮带的半宽(pt) +const GLOW_BASE: f64 = 0.4; +const GLOW_HALF: f64 = 6.0; + +/// 把图里 `x0..x1` 这一段已经画好的东西按流光的那一帧裁一遍透明度(DestinationIn): +/// 和菜单栏上的遮罩([`glow_mask`])同一个样子,给预览和截图用。**模板图只认透明度**, +/// 系统按菜单栏深浅着色,所以「亮」就是不透明:深色菜单栏上是一条白光,浅色上是一条黑影 +fn glow_paint(x0: f64, x1: f64, height: f64, phase: f64) { + let Some(ctx) = NSGraphicsContext::currentContext() else { + return; + }; + let (start, end) = (x0.floor(), x1.ceil()); + let center = start - GLOW_HALF + (end - start + 2.0 * GLOW_HALF) * phase; + let steps = ((end - start) * 4.0) as usize; + let stops: Vec> = (0..=steps) + .map(|i| { + let x = start + (end - start) * i as f64 / steps as f64; + let d = (x - center).abs() / GLOW_HALF; + NSColor::colorWithWhite_alpha(0.0, GLOW_BASE + (1.0 - GLOW_BASE) * lift(d)) + }) + .collect(); + let Some(gradient) = + NSGradient::initWithColors(NSGradient::alloc(), &NSArray::from_retained_slice(&stops)) + else { + return; + }; + ctx.setCompositingOperation(NSCompositingOperation::DestinationIn); + gradient.drawInRect_angle(rect(start, 0.0, end - start, height), 0.0); + ctx.setCompositingOperation(NSCompositingOperation::SourceOver); +} + fn fill_oval(x: f64, y: f64, d: f64, color: &NSColor) { color.setFill(); NSBezierPath::bezierPathWithOvalInRect(rect(x, y, d, d)).fill(); @@ -571,6 +770,7 @@ define_class!( let mtm = MainThreadMarker::from(self); menu.setAppearance(Some(&NSApplication::sharedApplication(mtm).effectiveAppearance())); OPEN.with(|o| o.set(true)); + sync_glow(mtm); if let Some(f) = ON_OPEN.with(|o| o.borrow().clone()) { f(true); } @@ -579,6 +779,7 @@ define_class!( #[unsafe(method(menuDidClose:))] fn menu_did_close(&self, _menu: &NSMenu) { OPEN.with(|o| o.set(false)); + sync_glow(MainThreadMarker::from(self)); if let Some(f) = ON_OPEN.with(|o| o.borrow().clone()) { f(false); } diff --git a/src-tauri/src/menubar/macos/preview.rs b/src-tauri/src/menubar/macos/preview.rs index e2ce5053..58f775bc 100644 --- a/src-tauri/src/menubar/macos/preview.rs +++ b/src-tauri/src/menubar/macos/preview.rs @@ -99,6 +99,11 @@ pub fn describe_menu() -> Vec { /// (`examples/menubar_preview.rs`)。**标准菜单项画的是近似的样子** —— 真的由 /// AppKit 画;自定义的几行和菜单栏那一块是真的。 pub fn preview_image(bar: &Bar, rows: &[Row], dark: bool) -> Retained { + preview_frame(bar, rows, dark, 0.5) +} + +/// 同 [`preview_image`],流光停在 `phase`(见 `glow_phase`):导出一串帧看流光怎么走 +pub fn preview_frame(bar: &Bar, rows: &[Row], dark: bool, phase: f64) -> Retained { use objc2_app_kit::{ NSAffineTransformNSAppKitAdditions, NSAppearance, NSAppearanceNameAqua, NSAppearanceNameDarkAqua, NSGraphicsContext, @@ -119,7 +124,7 @@ pub fn preview_image(bar: &Bar, rows: &[Row], dark: bool) -> Retained { .filter(|r| !matches!(r, Row::Separator | Row::Item(_))) .map(row_width) .fold(MENU_WIDTH, f64::max); - let bar_img = bar_image(bar); + let bar_img = bar_image(bar, Some(phase)); let bar_h = thickness() + 8.0; let total = bar_h + 10.0 + heights.iter().sum::() + 10.0; let width = menu_w + 24.0; diff --git a/src-tauri/src/menubar/model.rs b/src-tauri/src/menubar/model.rs index a7332414..227b3b63 100644 --- a/src-tauri/src/menubar/model.rs +++ b/src-tauri/src/menubar/model.rs @@ -304,8 +304,8 @@ pub struct Bar { pub numbers: Option<(String, String)>, /// 正在启动,或者不在运行 pub dim: bool, - /// 有请求在跑 - pub dot: bool, + /// 有请求在跑:标识上走流光 + pub busy: bool, /// 不在运行:红色角标(仅数值时破折号变红) pub alert: bool, /// 悬停提示,旁白读的也是它 @@ -573,7 +573,7 @@ fn bar(s: &Snapshot, style: Style) -> Bar { None }, dim: !running, - dot: running && !s.live.is_empty(), + busy: running && !s.live.is_empty(), alert: !running && !matches!(s.gateway, Gateway::Starting), tooltip: tooltip(s, tight), } diff --git a/src-tauri/src/menubar/model/tests.rs b/src-tauri/src/menubar/model/tests.rs index 17c89e55..9d8fb18d 100644 --- a/src-tauri/src/menubar/model/tests.rs +++ b/src-tauri/src/menubar/model/tests.rs @@ -354,7 +354,7 @@ fn the_bar_shows_todays_tokens_over_the_generation_speed() { s.rate = Some(42); let (bar, _) = build(&s, Style::Full); assert_eq!(bar.numbers, Some(("3.1M".into(), "42 t/s".into()))); - assert!(!bar.dim && !bar.alert && !bar.dot); + assert!(!bar.dim && !bar.alert && !bar.busy); with_lang(Lang::En, || { let tip = build(&s, Style::Full).0.tooltip; assert!(tip.contains("$41.20 cost, 42 tokens/s"), "{tip}"); @@ -735,7 +735,7 @@ fn requests_in_progress_put_a_dot_on_the_mark_and_list_at_most_five() { }) .collect(); let (bar, rows) = build(&s, Style::Full); - assert!(bar.dot); + assert!(bar.busy); let live: Vec<_> = rows .iter() .filter_map(|r| match r { @@ -786,7 +786,7 @@ fn a_gateway_that_is_not_running_says_why_and_offers_a_restart() { s.live = vec![Live::default()]; let (bar, rows) = build(&s, Style::Full); assert_eq!(bar.numbers, None, "不在运行时还显示一个凝固的旧数字"); - assert!(bar.dim && bar.alert && !bar.dot); + assert!(bar.dim && bar.alert && !bar.busy); assert!( matches!(&rows[2], Row::Status { state, tone: StateTone::Bad, .. } if state == "无法启动") );