Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions openless-all/app/src-tauri/src/coordinator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
//! insertion, persists history, emits `capsule:state` events to the capsule
//! window.

use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicU8, Ordering};
use std::sync::mpsc;
use std::sync::Arc;
use std::time::Instant;
Expand Down Expand Up @@ -55,8 +55,9 @@ use crate::recorder::{Recorder, RecorderError};
#[cfg(target_os = "windows")]
use crate::types::PasteShortcut;
use crate::types::{
CapsulePayload, CapsuleState, ChineseScriptPreference, DictationSession, HotkeyCapability,
HotkeyStatus, HotkeyStatusState, InsertStatus, OutputLanguagePreference, PolishMode,
CapsulePayload, CapsuleState, CapsuleStyle, ChineseScriptPreference, DictationSession,
HotkeyCapability, HotkeyStatus, HotkeyStatusState, InsertStatus, OutputLanguagePreference,
PolishMode,
};
#[cfg(target_os = "windows")]
use crate::windows_ime_ipc::ImeSubmitTarget;
Expand Down Expand Up @@ -554,6 +555,14 @@ struct Inner {
/// `warming` 打成 true(前端渲染"待命"光效);`level_handler` 首次触发(PCM 真的
/// 流入)后置 false,光条"点亮"进入正式录音。begin_session 每次入场重置为 true。
capsule_warming: AtomicBool,
/// 用户选择的胶囊样式缓存(0=Siri,1=Classic)。emit_capsule 在音频回调线程
/// ~30Hz 读它下发 payload.capsuleStyle;主线程闭包每帧从 prefs 同步该值——
/// 读偏好锁的代价只落在主线程(与 show_capsule 同源),音频线程零开销。
capsule_style: AtomicU8,
/// 胶囊窗口当前是否鼠标穿透(true=穿透)。经典药丸需要接收 ✕/✓ 点击时,主线程
/// 闭包把它翻 false;离开可交互状态立即恢复 true。初始 true 与 lib.rs 启动时
/// set_ignore_cursor_events(true) 保持一致。
capsule_cursor_passthrough: AtomicBool,
/// QA 用的 ASR 句柄。必须跟 active_asr_provider 保持一致,避免浮窗走不同入口。
qa_asr: Mutex<Option<SessionResource<ActiveAsr>>>,
/// QA 用的 Recorder 句柄。
Expand Down Expand Up @@ -749,6 +758,8 @@ impl Coordinator {
qa_state: Mutex::new(QaSessionState::default()),
capsule_layout: Mutex::new(None),
capsule_warming: AtomicBool::new(false),
capsule_style: AtomicU8::new(0),
capsule_cursor_passthrough: AtomicBool::new(true),
qa_asr: Mutex::new(None),
qa_recorder: Mutex::new(None),
qa_stream_cancelled: Arc::new(AtomicBool::new(false)),
Expand Down Expand Up @@ -863,6 +874,8 @@ impl Coordinator {
qa_state: Mutex::new(QaSessionState::default()),
capsule_layout: Mutex::new(None),
capsule_warming: AtomicBool::new(false),
capsule_style: AtomicU8::new(0),
capsule_cursor_passthrough: AtomicBool::new(true),
qa_asr: Mutex::new(None),
qa_recorder: Mutex::new(None),
qa_stream_cancelled: Arc::new(AtomicBool::new(false)),
Expand Down
41 changes: 40 additions & 1 deletion openless-all/app/src-tauri/src/coordinator/capsule_focus.rs
Original file line number Diff line number Diff line change
Expand Up @@ -534,6 +534,12 @@ fn emit_capsule_with_context_locked(
operating,
warming,
selection_polish,
// 用户选择的胶囊样式:读 Inner 上的原子缓存(主线程闭包每帧从 prefs 同步),
// 不在音频回调线程碰偏好锁。设置里切换后下一次录音即生效。
capsule_style: match inner.capsule_style.load(Ordering::Relaxed) {
1 => CapsuleStyle::Classic,
_ => CapsuleStyle::Siri,
},
};

#[cfg(target_os = "android")]
Expand Down Expand Up @@ -652,6 +658,9 @@ fn emit_capsule_with_context_locked(
let app_for_main = app.clone();
// 入场帧要在 window.show 之后、闭包内部把 state 回发给前端,需要 payload 的独立副本
// move 进闭包;非入场帧走闭包外的即时同步 emit(下方),这里就是 None。
// 注意:入场帧的 payload 在闭包同步 capsule_style 原子之前克隆,最多带一帧旧样式
//(设置里刚切换后的首次录音,第 2 帧 ~33ms 即纠正)。这是刻意取舍——不要在音频
// 线程改回直接读 prefs。前端第 1 帧处于 capsule-in 动画期间(380ms),无感知。
let payload_for_deferred_emit = if defer_capsule_emit {
Some(payload.clone())
} else {
Expand All @@ -671,7 +680,16 @@ fn emit_capsule_with_context_locked(
};
// `show_capsule` 是原有“录音胶囊”偏好;Selection Polish 没有独立开关,且它的
// 无选区/失败提示是这条无界面工作流的唯一反馈,所以始终展示轻量提示。
let show_capsule = selection_polish || inner_for_main.prefs.get().show_capsule;
let prefs_snapshot = inner_for_main.prefs.get();
let show_capsule = selection_polish || prefs_snapshot.show_capsule;
// 把胶囊样式同步进 Inner 原子缓存:emit_capsule(音频回调线程)从这里读
// payload.capsuleStyle,避免在音频线程碰偏好锁。主线程每帧克隆 prefs 本就有
//(show_capsule 同源),多读一个字段零额外代价。
let classic_style = matches!(prefs_snapshot.capsule_style, CapsuleStyle::Classic);
inner_for_main.capsule_style.store(
if classic_style { 1 } else { 0 },
Ordering::Relaxed,
);
// Linux: 不操作胶囊窗口(不 show/hide,不 reposition)。
// 文字通过 fcitx5 插件直接 commit,用户始终在目标 app 中。
#[cfg(target_os = "linux")]
Expand All @@ -687,6 +705,27 @@ fn emit_capsule_with_context_locked(
// `hide_capsule_window_if_present()` Win32 hard-hide 在 visible=false 分支
// 处理,不依赖把 Done/Cancelled/Error 打成 invisible。详见 PR #140 评论。
maybe_position_capsule_bottom_center(&inner_for_main, &window, translation);
// 经典药丸(Openless 默认风格)的 ✕/✓ 按钮需要接收点击:录音/转写/润色期间
// 关掉鼠标穿透(按钮可点,代价是窗口底部 460×180 区域在这几秒内拦截点击——
// 与 1.3.x 经典胶囊同款取舍);终态 toast、隐藏、Siri 光效、选区润色提示一律
// 保持穿透,不遮挡底层 app。set_ignore_cursor_events 只在值变化时调用一次。
let interactive = classic_style
&& visible
&& !selection_polish
&& matches!(
state,
CapsuleState::Recording | CapsuleState::Transcribing | CapsuleState::Polishing
);
let want_passthrough = !interactive;
if inner_for_main
.capsule_cursor_passthrough
.swap(want_passthrough, Ordering::SeqCst)
!= want_passthrough
{
if let Err(e) = window.set_ignore_cursor_events(want_passthrough) {
log::warn!("[capsule] set_ignore_cursor_events failed: {e}");
}
}
if show_capsule && visible {
// 用户报"看不到胶囊"时第一时间能在 log 里确认:胶囊路径有跑、show_capsule
// 开关是 true、当前进入 visible 帧 —— 排除 prefs 没存住 / emit_capsule 没触
Expand Down
25 changes: 25 additions & 0 deletions openless-all/app/src-tauri/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,10 @@ pub struct UserPreferences {
pub custom_style_prompts: CustomStylePrompts,
pub launch_at_login: bool,
pub show_capsule: bool,
/// 录音胶囊样式:'siri' = 流光 Siri 光效版(默认);'classic' = Openless 经典药丸版。
/// 由 capsule:state 事件的 capsuleStyle 字段下发到胶囊 webview,下次录音即生效。
#[serde(default)]
pub capsule_style: CapsuleStyle,
/// 录音期间临时静音系统输出,停止/取消/出错后恢复原静音状态。
#[serde(default)]
pub mute_during_recording: bool,
Expand Down Expand Up @@ -1035,6 +1039,8 @@ struct UserPreferencesWire {
launch_at_login: bool,
show_capsule: bool,
#[serde(default)]
capsule_style: CapsuleStyle,
#[serde(default)]
mute_during_recording: bool,
#[serde(default = "default_true")]
audio_cue_on_record: bool,
Expand Down Expand Up @@ -1200,6 +1206,7 @@ impl Default for UserPreferencesWire {
custom_style_prompts: prefs.custom_style_prompts,
launch_at_login: prefs.launch_at_login,
show_capsule: prefs.show_capsule,
capsule_style: prefs.capsule_style,
mute_during_recording: prefs.mute_during_recording,
audio_cue_on_record: prefs.audio_cue_on_record,
microphone_device_name: prefs.microphone_device_name,
Expand Down Expand Up @@ -1323,6 +1330,7 @@ impl<'de> Deserialize<'de> for UserPreferences {
custom_style_prompts: wire.custom_style_prompts,
launch_at_login: wire.launch_at_login,
show_capsule: wire.show_capsule,
capsule_style: wire.capsule_style,
mute_during_recording: wire.mute_during_recording,
audio_cue_on_record: wire.audio_cue_on_record,
microphone_device_name: wire.microphone_device_name,
Expand Down Expand Up @@ -2140,6 +2148,7 @@ impl Default for UserPreferences {
custom_style_prompts: CustomStylePrompts::default(),
launch_at_login: false,
show_capsule: true,
capsule_style: CapsuleStyle::Siri,
mute_during_recording: false,
audio_cue_on_record: true,
microphone_device_name: String::new(),
Expand Down Expand Up @@ -2855,6 +2864,18 @@ pub enum CapsuleState {
Error,
}

/// 录音胶囊样式。由 UserPreferences.capsule_style 透传到 capsule:state payload,
/// 胶囊 webview 据此选择渲染流光 Siri 光效舞台还是经典药丸。
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub enum CapsuleStyle {
/// 流光 Siri 风格:SiriGL 光效舞台(默认)。
#[default]
Siri,
/// Openless 默认风格:经典毛玻璃药丸(音量条 + 取消/确认按钮)。
Classic,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CapsulePayload {
Expand All @@ -2876,6 +2897,10 @@ pub struct CapsulePayload {
/// false,光条"点亮"进入正式录音态。只对 Recording 状态有意义。详见胶囊出现时序改造。
#[serde(default)]
pub warming: bool,
/// 用户选择的胶囊样式(siri / classic)。随每次状态事件下发,设置里切换后下一次
/// 录音即生效,胶囊 webview 无需额外请求。
#[serde(default)]
pub capsule_style: CapsuleStyle,
/// 选区润色专用的轻量反馈。它与原有语音/QA 会话共用同一扇不抢焦点的 capsule
/// 窗口,但前端据此切换为一行状态提示,避免改变既有语音光效与文案。
#[serde(default)]
Expand Down
Loading
Loading